[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**System information**\nOutput of `python -c 'from flower.utils import bugreport; print(bugreport())'` command\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot\n# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem\nversion: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"  # Group all Actions updates into a single larger pull request\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12']\n        celery-version: ['5.2.*', '5.3.*', '5.4.*', '5.5.*']\n        tornado-version: ['6.0']\n        exclude:  # https://docs.celeryq.dev/en/v5.2.7/whatsnew-5.2.html#step-5-upgrade-to-celery-5-2\n          - python-version: '3.12'\n            celery-version: '5.2.*'\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Set up python\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install celery==${{ matrix.celery-version }} \\\n                    tornado==${{ matrix.tornado-version }} \\\n                    -r requirements/dev.txt\n\n\n    - name: Run unit tests\n      run: |\n        python -m flower --version\n        python -m tests.unit\n\n    - name: Lint with pylint\n      run: |\n        pylint flower --rcfile .pylintrc\n\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: docker\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - '*'\n  pull_request:\n    branches:\n      - master\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: docker/setup-qemu-action@v4\n\n      - uses: docker/setup-buildx-action@v4\n\n      - id: meta\n        uses: docker/metadata-action@v6\n        with:\n          images: mher/flower\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n\n      - uses: docker/login-action@v4\n        if: github.event_name != 'pull_request'\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - uses: docker/build-push-action@v7\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.pyc\n*$py.class\n*~\n.*.sw[po]\ndist/\n*.egg-info\n*.egg\n*.egg/\n*.eggs\ndoc/__build/*\nbuild/\n.build/\npip-log.txt\n.directory\nerl_crash.dump\n*.db\nDocumentation/\n.tox/\n.ropeproject/\n.project\n.pydevproject\n.idea\n.vagrant\nenv\nvenv\n*.retry\n.cache/\n.mypy_cache/\nPipfile*\n.vscode/\ndata/\n.python-version\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MASTER]\ndisable=\n    C0114, # missing-module-docstring\n    C0115, # missing-class-docstring\n    C0116, # missing-function-docstring\n    C0301, # line-too-long\n    W0223, # abstract-method\n    R0903, # too-few-public-methods\n    R0902, # too-many-instance-attributes\n    W0622, # redefined-builtin\n    C0415, # import-outside-toplevel\n    W0718, # broad-exception-caught\n    R1735, # use-dict-literal\n    R0917, # too-many-positional-arguments\n\n[BASIC]\ngood-names=i,e,n,x,logger,tz,db,dt\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# .readthedocs.yaml\n# Read the Docs configuration file\n\nversion: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n  install:\n    - requirements: requirements/docs.txt\n"
  },
  {
    "path": "CONTRIBUTORS",
    "content": "======================================\n Contributors (in chronological order)\n======================================\n\nMher Movsisyan\nAsk Solem\nLukasz Marcin Dobrzanski\nAlexander Koshelev\nGary Linscott\nTommaso Barbugli\nMiguel Gaiowski\nMatt Hughes\nRomain Commandé\nAndres Riancho\nJet Zheung\nAudrius Butkevicius\nYulian Slobodyan\nRob O'Dwyer\nHorace Thomas\nKit Sunde\nAdam Greig\nLuciano Pacheco\nMiki Tebeka\nMichael J. Schultz\nTJ Kells\nGeoff Jukes\nPeter De Vries\nLisa Chung\nSabeel Saif Hakim\nGaurav Dadhania\nCharlie Marshall\nBenjamin Drung\nDavid Thorman\nHong Minhee\nJohn Costa\nIuri de Silvio\nBalthazar Rouberol\nAlexandre Ferland\nFlorian Glesser\nTomasz Pazurkiewicz\nBenjamin Toueg\nRob Hoelz\nTadej Janež\nCorey Farwell\nThomas Grainger\nTom Mortimer-Jones\nKonstantinos Koukopoulos\nSamuel Cormier-Iijima\nDavid Matson\nPaulo SantAnna\nSanchit Arora\nIlya Lebedev\nWendy Liu\nMike Helmick\nIlya Georgievsky\nRaghuram Onti Srinivasan\nMichael Kahn\nGaurav Kumar\nSimon Westphahl\nPedro Ferreira\nDanilo Resende\nKevin Wu\nVinay Karanam\nRodrigo Pinheiro Matias\nThomas Boquet\nMisha Behersky\nSebastian Kalinowski\nJingyu Zhou\nMaxim Krivodaev\nAlli Witheford\nAlexander Zaitsev\nAnton Prokhorov\nSharang Phadke\nMoinuddin Quadri\nJohn Arnold\nScott Kruger\nDavid Schneider\nS M Ahasanul Haque\nLeo Singer\nPavel Savchenko\nBhargav Srinivasan\nJosiah Berkebile\nDeniz Dogan\nAliaksei Urbanski\nMike Dearman\nFrancisco J. Capdevila\nScott Allen\nJohan Adami\nRay Marc Marcellones\nWen YE\nWaleed Darwish\nBjorn Stiel\nFabio Todaro\nSimon Gurcke\nJason Held\nLukas Matta\nTomasz Kluczkowski\nAlexey Nikitenko\nSergey Klyuykov\nLouis Frament"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:alpine\n\n# Get latest root certificates and update openssl to fix vulnerabilities\nRUN apk add --no-cache ca-certificates tzdata && \\\n    apk upgrade --no-cache openssl && \\\n    update-ca-certificates\n\n# Install the required packages\nRUN pip install --no-cache-dir redis flower\n\n# PYTHONUNBUFFERED: Force stdin, stdout and stderr to be totally unbuffered. (equivalent to `python -u`)\n# PYTHONHASHSEED: Enable hash randomization (equivalent to `python -R`)\n# PYTHONDONTWRITEBYTECODE: Do not write byte files to disk, since we maintain it as readonly. (equivalent to `python -B`)\nENV PYTHONUNBUFFERED=1 PYTHONHASHSEED=random PYTHONDONTWRITEBYTECODE=1\n\n# Default port\nEXPOSE 5555\n\nENV FLOWER_DATA_DIR /data\nENV PYTHONPATH ${FLOWER_DATA_DIR}\n\nWORKDIR $FLOWER_DATA_DIR\n\n# Add a user with an explicit UID/GID and create necessary directories\nRUN set -eux; \\\n    addgroup -g 1000 flower; \\\n    adduser -u 1000 -G flower flower -D; \\\n    mkdir -p \"$FLOWER_DATA_DIR\"; \\\n    chown flower:flower \"$FLOWER_DATA_DIR\"\nUSER flower\n\nVOLUME $FLOWER_DATA_DIR\n\nCMD [\"celery\", \"flower\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2012, Mher Movsisyan and individual contributors.\nAll rights reserved. \n\nRedistribution and use in source and binary forms, with or without \nmodification, are permitted provided that the following conditions are met: \n\n * Redistributions of source code must retain the above copyright notice, \n   this list of conditions and the following disclaimer. \n\n * Redistributions in binary form must reproduce the above copyright \n   notice, this list of conditions and the following disclaimer in the \n   documentation and/or other materials provided with the distribution. \n\n * Neither the name of the Celery Flower nor the names of its contributors \n   may be used to endorse or promote products derived from this software\n   without specific prior written permission. \n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" \nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE \nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE \nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE \nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR \nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF \nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS \nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN \nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) \nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE \nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include AUTHORS\ninclude CHANGES\ninclude LICENSE\ninclude MANIFEST.in\ninclude README.rst\nrecursive-include docs *\nrecursive-include flower/static *\nrecursive-include flower/templates *\nrecursive-include tests *\nrecursive-include requirements *.txt\n"
  },
  {
    "path": "README.rst",
    "content": "Flower\n======\n\n.. image:: https://img.shields.io/pypi/dm/flower.svg\n    :target: https://pypistats.org/packages/flower\n    :alt: PyPI - Downloads\n.. image:: https://img.shields.io/docker/pulls/mher/flower.svg\n    :target: https://hub.docker.com/r/mher/flower\n    :alt: Docker Pulls\n.. image:: https://github.com/mher/flower/workflows/Build/badge.svg\n    :target: https://github.com/mher/flower/actions\n.. image:: https://img.shields.io/pypi/v/flower.svg\n    :target: https://pypi.python.org/pypi/flower\n\nFlower is an open-source web application for monitoring and managing Celery clusters.\nIt provides real-time information about the status of Celery workers and tasks.\n\nFeatures\n--------\n\n- Real-time monitoring using Celery Events\n    - View task progress and history\n    - View task details (arguments, start time, runtime, and more)\n- Remote Control\n    - View worker status and statistics\n    - Shutdown and restart worker instances\n    - Control worker pool size and autoscale settings\n    - View and modify the queues a worker instance consumes from\n    - View currently running tasks\n    - View scheduled tasks (ETA/countdown)\n    - View reserved and revoked tasks\n    - Apply time and rate limits\n    - Revoke or terminate tasks\n- Broker monitoring\n    - View statistics for all Celery queues\n- HTTP Basic Auth, Google, Github, Gitlab and Okta OAuth\n- Prometheus integration\n- API\n\nInstallation\n------------\n\nInstalling `flower` with `pip <http://www.pip-installer.org/>`_ is simple ::\n\n    $ pip install flower\n\nThe development version can be installed from Github ::\n\n    $ pip install https://github.com/mher/flower/zipball/master#egg=flower\n\nUsage\n-----\n\nTo run Flower, you need to provide the broker URL ::\n\n    $ celery --broker=amqp://guest:guest@localhost:5672// flower\n\nOr use the configuration of `celery application <https://docs.celeryq.dev/en/stable/userguide/application.html>`_  ::\n\n    $ celery -A tasks.app flower\n\nBy default, flower runs on port 5555, which can be modified with the `port` option ::\n\n    $ celery -A tasks.app flower --port=5001\n\nYou can also run Flower using the docker image ::\n\n    $ docker run -v examples:/data -p 5555:5555 mher/flower celery --app=tasks.app flower\n\nIn this example, Flower is using the `tasks.app` defined in the `examples/tasks.py <https://github.com/mher/flower/blob/master/examples/tasks.py>`_ file\n\nAPI\n---\n\nFlower API enables to manage the cluster via HTTP `REST API`.\n\nFor example you can restart worker's pool by: ::\n\n    $ curl -X POST http://localhost:5555/api/worker/pool/restart/myworker\n\nOr call a task by: ::\n\n    $ curl -X POST -d '{\"args\":[1,2]}' http://localhost:5555/api/task/async-apply/tasks.add\n\nOr terminate executing task by: ::\n\n    $ curl -X POST -d 'terminate=True' http://localhost:5555/api/task/revoke/8a4da87b-e12b-4547-b89a-e92e4d1f8efd\n\nFor more info checkout `API Reference`_\n\n.. _API Reference: https://flower.readthedocs.io/en/latest/api.html\n\nDocumentation\n-------------\n\nDocumentation is available at `Read the Docs`_\n\n.. _Read the Docs: https://flower.readthedocs.io\n\nLicense\n-------\n\nFlower is licensed under BSD 3-Clause License.\nSee the `License`_ file for the full license text.\n\n.. _`License`: https://github.com/mher/flower/blob/master/LICENSE\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  redis:\n    image: redis:alpine\n    ports:\n      - 6379:6379\n  prometheus:\n    image: prom/prometheus\n    volumes:\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n    ports:\n      - 9090:9090\n  grafana:\n    image: grafana/grafana\n    depends_on:\n      - prometheus\n    ports:\n      - 3000:3000\n  worker:\n    build: ./\n    entrypoint: celery\n    command: -A tasks worker -l info -E\n    user: nobody\n    volumes:\n      - ./examples:/data\n    environment:\n      CELERY_BROKER_URL: redis://redis\n      CELERY_RESULT_BACKEND: redis://redis\n      PYTHONPATH: /data\n    depends_on:\n      - redis\n  flower:\n    build: ./\n    command: celery -A tasks flower\n    volumes:\n      - ./examples:/data\n    working_dir: /data\n    ports:\n      - 5555:5555\n    environment:\n      CELERY_BROKER_URL: redis://redis\n      CELERY_RESULT_BACKEND: redis://redis\n    depends_on:\n      - worker\n      - redis"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = .build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/flower.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/flower.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/flower\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flower\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "docs/_static/.keep",
    "content": ""
  },
  {
    "path": "docs/_templates/localtoc.html",
    "content": "{%- if display_toc %}\n  <h3><a href=\"{{ pathto(master_doc) }}\">{{ _('Table Of Contents') }}</a></h3>\n  {{ toctree(collapse=False) }}\n{%- endif %}\n"
  },
  {
    "path": "docs/_templates/page.html",
    "content": "{% extends \"layout.html\" %}\n{% block body %}\n<div class=\"deck\">\n\n</div>\n    {{ body }}\n{% endblock %}\n"
  },
  {
    "path": "docs/_templates/sidebarintro.html",
    "content": "<p>\n  <iframe src=\"http://ghbtns.com/github-btn.html?user=mher&repo=flower&type=watch&count=true&size=large\"\n    allowtransparency=\"true\" frameborder=\"0\" scrolling=\"0\" width=\"200px\" height=\"35px\"></iframe>\n</p>\n{%- if display_toc %}\n  <h3><a href=\"{{ pathto(master_doc) }}\">{{ _('Table Of Contents') }}</a></h3>\n  {{ toctree(collapse=False) }}\n{%- endif %}\n"
  },
  {
    "path": "docs/_templates/sidebarlogo.html",
    "content": "<p>\n  <iframe src=\"http://ghbtns.com/github-btn.html?user=mher&repo=flower&type=watch&count=true&size=large\"\n    allowtransparency=\"true\" frameborder=\"0\" scrolling=\"0\" width=\"200px\" height=\"35px\"></iframe>\n</p>\n"
  },
  {
    "path": "docs/_theme/celery/static/celery.css_t",
    "content": "/*\n * celery.css_t\n * ~~~~~~~~~~~~\n *\n * :copyright: Copyright 2010 by Armin Ronacher.\n * :license: BSD, see LICENSE for details.\n */\n\n{% set page_width = 940 %}\n{% set sidebar_width = 220 %}\n{% set body_font_stack = 'Optima, Segoe, \"Segoe UI\", Candara, Calibri, Arial, sans-serif' %}\n{% set headline_font_stack = 'Futura, \"Trebuchet MS\", Arial, sans-serif' %}\n{% set code_font_stack = \"'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace\" %}\n\n@import url(\"basic.css\");\n\n/* -- page layout ----------------------------------------------------------- */\n\nbody {\n    font-family: {{ body_font_stack }};\n    font-size: 17px;\n    background-color: white;\n    color: #000;\n    margin: 30px 0 0 0;\n    padding: 0;\n}\n\ndiv.document {\n    width: {{ page_width }}px;\n    margin: 0 auto;\n}\n\ndiv.deck {\n    font-size: 18px;\n}\n\np.developmentversion {\n    color: red;\n}\n\ndiv.related {\n    width: {{ page_width - 20 }}px;\n    padding: 5px 10px;\n    background: #F2FCEE;\n    margin: 15px auto 15px auto;\n}\n\ndiv.documentwrapper {\n    float: left;\n    width: 100%;\n}\n\ndiv.bodywrapper {\n    margin: 0 0 0 {{ sidebar_width }}px;\n}\n\ndiv.sphinxsidebar {\n    width: {{ sidebar_width }}px;\n}\n\nhr {\n    border: 1px solid #B1B4B6;\n}\n\ndiv.body {\n    background-color: #ffffff;\n    color: #3E4349;\n    padding: 0 30px 0 30px;\n}\n\nimg.celerylogo {\n    padding: 0 0 10px 10px;\n    float: right;\n}\n\ndiv.footer {\n    width: {{ page_width - 15 }}px;\n    margin: 10px auto 30px auto;\n    padding-right: 15px;\n    font-size: 14px;\n    color: #888;\n    text-align: right;\n}\n\ndiv.footer a {\n    color: #888;\n}\n\ndiv.sphinxsidebar a {\n    color: #444;\n    text-decoration: none;\n    border-bottom: 1px dashed #DCF0D5;\n}\n\ndiv.sphinxsidebar a:hover {\n    border-bottom: 1px solid #999;\n}\n\ndiv.sphinxsidebar {\n    font-size: 14px;\n    line-height: 1.5;\n}\n\ndiv.sphinxsidebarwrapper {\n    padding: 7px 10px;\n}\n\ndiv.sphinxsidebarwrapper p.logo {\n    padding: 0 0 20px 0;\n    margin: 0;\n}\n\ndiv.sphinxsidebar h3,\ndiv.sphinxsidebar h4 {\n    font-family: {{ headline_font_stack }};\n    color: #444;\n    font-size: 24px;\n    font-weight: normal;\n    margin: 0 0 5px 0;\n    padding: 0;\n}\n\ndiv.sphinxsidebar h4 {\n    font-size: 20px;\n}\n\ndiv.sphinxsidebar h3 a {\n    color: #444;\n}\n\ndiv.sphinxsidebar p.logo a,\ndiv.sphinxsidebar h3 a,\ndiv.sphinxsidebar p.logo a:hover,\ndiv.sphinxsidebar h3 a:hover {\n    border: none;\n}\n\ndiv.sphinxsidebar p {\n    color: #555;\n    margin: 10px 0;\n}\n\ndiv.sphinxsidebar ul {\n    margin: 10px 0;\n    padding: 0;\n    color: #000;\n}\n\ndiv.sphinxsidebar input {\n    border: 1px solid #ccc;\n    font-family: {{ body_font_stack }};\n    font-size: 1em;\n}\n\n/* -- body styles ----------------------------------------------------------- */\n\na {\n    color: #348613;\n    text-decoration: underline;\n}\n\na:hover {\n    color: #59B833;\n    text-decoration: underline;\n}\n\ndiv.body h1,\ndiv.body h2,\ndiv.body h3,\ndiv.body h4,\ndiv.body h5,\ndiv.body h6 {\n    font-family: {{ headline_font_stack }};\n    font-weight: normal;\n    margin: 30px 0px 10px 0px;\n    padding: 0;\n}\n\ndiv.body h1 { margin-top: 0; padding-top: 0; font-size: 200%; }\ndiv.body h2 { font-size: 180%; }\ndiv.body h3 { font-size: 150%; }\ndiv.body h4 { font-size: 130%; }\ndiv.body h5 { font-size: 100%; }\ndiv.body h6 { font-size: 100%; }\n\ndiv.body h1 a.toc-backref,\ndiv.body h2 a.toc-backref,\ndiv.body h3 a.toc-backref,\ndiv.body h4 a.toc-backref,\ndiv.body h5 a.toc-backref,\ndiv.body h6 a.toc-backref {\n    color: inherit!important;\n    text-decoration: none;\n}\n\na.headerlink {\n    color: #ddd;\n    padding: 0 4px;\n    text-decoration: none;\n}\n\na.headerlink:hover {\n    color: #444;\n    background: #eaeaea;\n}\n\ndiv.body p, div.body dd, div.body li {\n    line-height: 1.4em;\n}\n\ndiv.admonition {\n    background: #fafafa;\n    margin: 20px -30px;\n    padding: 10px 30px;\n    border-top: 1px solid #ccc;\n    border-bottom: 1px solid #ccc;\n}\n\ndiv.admonition p.admonition-title {\n    font-family: {{ headline_font_stack }};\n    font-weight: normal;\n    font-size: 24px;\n    margin: 0 0 10px 0;\n    padding: 0;\n    line-height: 1;\n}\n\ndiv.admonition p.last {\n    margin-bottom: 0;\n}\n\ndiv.highlight{\n    background-color: white;\n}\n\ndt:target, .highlight {\n    background: #FAF3E8;\n}\n\ndiv.note {\n    background-color: #eee;\n    border: 1px solid #ccc;\n}\n\ndiv.seealso {\n    background-color: #ffc;\n    border: 1px solid #ff6;\n}\n\ndiv.topic {\n    background-color: #eee;\n}\n\ndiv.warning {\n    background-color: #ffe4e4;\n    border: 1px solid #f66;\n}\n\np.admonition-title {\n    display: inline;\n}\n\np.admonition-title:after {\n    content: \":\";\n}\n\npre, tt {\n    font-family: {{ code_font_stack }};\n    font-size: 0.9em;\n}\n\nimg.screenshot {\n}\n\ntt.descname, tt.descclassname {\n    font-size: 0.95em;\n}\n\ntt.descname {\n    padding-right: 0.08em;\n}\n\nimg.screenshot {\n    -moz-box-shadow: 2px 2px 4px #eee;\n    -webkit-box-shadow: 2px 2px 4px #eee;\n    box-shadow: 2px 2px 4px #eee;\n}\n\ntable.docutils {\n    border: 1px solid #888;\n    -moz-box-shadow: 2px 2px 4px #eee;\n    -webkit-box-shadow: 2px 2px 4px #eee;\n    box-shadow: 2px 2px 4px #eee;\n}\n\ntable.docutils td, table.docutils th {\n    border: 1px solid #888;\n    padding: 0.25em 0.7em;\n}\n\ntable.field-list, table.footnote {\n    border: none;\n    -moz-box-shadow: none;\n    -webkit-box-shadow: none;\n    box-shadow: none;\n}\n\ntable.footnote {\n    margin: 15px 0;\n    width: 100%;\n    border: 1px solid #eee;\n    background: #fdfdfd;\n    font-size: 0.9em;\n}\n\ntable.footnote + table.footnote {\n    margin-top: -15px;\n    border-top: none;\n}\n\ntable.field-list th {\n    padding: 0 0.8em 0 0;\n}\n\ntable.field-list td {\n    padding: 0;\n}\n\ntable.footnote td.label {\n    width: 0px;\n    padding: 0.3em 0 0.3em 0.5em;\n}\n\ntable.footnote td {\n    padding: 0.3em 0.5em;\n}\n\ndl {\n    margin: 0;\n    padding: 0;\n}\n\ndl dd {\n    margin-left: 30px;\n}\n\nblockquote {\n    margin: 0 0 0 30px;\n    padding: 0;\n}\n\nul {\n    margin: 10px 0 10px 30px;\n    padding: 0;\n}\n\npre {\n    background: #F0FFEB;\n    padding: 7px 10px;\n    margin: 15px 0;\n    border: 1px solid #C7ECB8;\n    border-radius: 2px;\n    -moz-border-radius: 2px;\n    -webkit-border-radius: 2px;\n    line-height: 1.3em;\n}\n\ntt {\n    background: #F0FFEB;\n    color: #222;\n    /* padding: 1px 2px; */\n}\n\ntt.xref, a tt {\n    background: #F0FFEB;\n    border-bottom: 1px solid white;\n}\n\na.reference {\n    text-decoration: none;\n    border-bottom: 1px dashed #DCF0D5;\n}\n\na.reference:hover {\n    border-bottom: 1px solid #6D4100;\n}\n\na.footnote-reference {\n    text-decoration: none;\n    font-size: 0.7em;\n    vertical-align: top;\n    border-bottom: 1px dashed #DCF0D5;\n}\n\na.footnote-reference:hover {\n    border-bottom: 1px solid #6D4100;\n}\n\na:hover tt {\n    background: #EEE;\n}\n"
  },
  {
    "path": "docs/_theme/celery/theme.conf",
    "content": "[theme]\ninherit = basic\nstylesheet = celery.css\n\n[options]\n"
  },
  {
    "path": "docs/api.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# flower REST API\\n\",\n    \"\\n\",\n    \"This document shows how to use the flower [REST API](https://github.com/mher/flower#api). \\n\",\n    \"\\n\",\n    \"We will use [requests](http://www.python-requests.org/en/latest/) for accessing the API. (See [here](http://www.python-requests.org/en/latest/user/install/) on how to install it.)    \"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Code\\n\",\n    \"We'll use the following code throughout the documentation.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## tasks.py\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 43,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from celery import Celery\\n\",\n    \"from time import sleep\\n\",\n    \"\\n\",\n    \"celery = Celery()\\n\",\n    \"celery.config_from_object({\\n\",\n    \"    'BROKER_URL': 'amqp://localhost',\\n\",\n    \"    'CELERY_RESULT_BACKEND': 'amqp://',\\n\",\n    \"    'CELERYD_POOL_RESTARTS': True,  # Required for /worker/pool/restart API\\n\",\n    \"})\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@celery.task\\n\",\n    \"def add(x, y):\\n\",\n    \"    return x + y\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@celery.task\\n\",\n    \"def sub(x, y):\\n\",\n    \"    sleep(30)  # Simulate work\\n\",\n    \"    return x -  y\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Running\\n\",\n    \"You'll need a celery worker instance and a flower instance running. In one terminal window run\\n\",\n    \"\\n\",\n    \"    celery worker --loglevel INFO -A proj -E --autoscale 10,3\\n\",\n    \"\\n\",\n    \"and in another terminal run\\n\",\n    \"\\n\",\n    \"    celery flower -A proj\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Tasks API\\n\",\n    \"The tasks API is *async*, meaning calls will return immediately and you'll need to poll on task status.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Done once for the whole docs\\n\",\n    \"import requests, json\\n\",\n    \"api_root = 'http://localhost:5555/api'\\n\",\n    \"task_api = '{}/task'.format(api_root)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## async-apply\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/async-apply/tasks.add\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'state': u'PENDING', u'task-id': u'f4a53407-30f3-42af-869f-b7f8f4fbd684'}\"\n      ]\n     },\n     \"execution_count\": 6,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"args = {'args': [1, 2]}\\n\",\n    \"url = '{}/async-apply/tasks.add'.format(task_api)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, data=json.dumps(args))\\n\",\n    \"reply = resp.json()\\n\",\n    \"reply\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"We can see that we created a new task and it's pending. Note that the API is *async*, meaning it won't wait until the task finish.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## apply\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"For create task and wait results you can use 'apply' API.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/apply/tasks.add\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'result': 3,\\n\",\n       \" u'state': u'SUCCESS',\\n\",\n       \" u'task-id': u'ced6fd57-419e-4b8e-8d99-0770be717cb4'}\"\n      ]\n     },\n     \"execution_count\": 7,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"args = {'args': [1, 2]}\\n\",\n    \"url = '{}/apply/tasks.add'.format(task_api)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, data=json.dumps(args))\\n\",\n    \"reply = resp.json()\\n\",\n    \"reply\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## result\\n\",\n    \"Gets the task result. This is *async* and will return immediately even if the task didn't finish (with state 'PENDING')\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/result/ced6fd57-419e-4b8e-8d99-0770be717cb4\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'result': 3,\\n\",\n       \" u'state': u'SUCCESS',\\n\",\n       \" u'task-id': u'ced6fd57-419e-4b8e-8d99-0770be717cb4'}\"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/result/{}'.format(task_api, reply['task-id'])\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.get(url)\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## revoke\\n\",\n    \"Revoke a running task.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/revoke/bcb4ac2e-cb2d-4a4b-a402-8eb3a3b0c8e8\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"Revoked 'bcb4ac2e-cb2d-4a4b-a402-8eb3a3b0c8e8'\\\"}\"\n      ]\n     },\n     \"execution_count\": 7,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Run a task\\n\",\n    \"args = {'args': [1, 2]}\\n\",\n    \"resp = requests.post('{}/async-apply/tasks.sub'.format(task_api), data=json.dumps(args))\\n\",\n    \"reply = resp.json()\\n\",\n    \"\\n\",\n    \"# Now revoke it\\n\",\n    \"url = '{}/revoke/{}'.format(task_api, reply['task-id'])\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, data='terminate=True')\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## rate-limit\\n\",\n    \"Update [rate limit](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.rate_limit) for a task.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/rate-limit/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u'new rate limit set successfully'}\"\n      ]\n     },\n     \"execution_count\": 20,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"worker = 'miki-manjaro'  # You'll need to get the worker name from the worker API (seel below)\\n\",\n    \"url = '{}/rate-limit/{}'.format(task_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'taskname': 'tasks.add', 'ratelimit': '10'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## timeout\\n\",\n    \"Set timeout (both [hard](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.time_limit) and [soft](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.soft_time_limit)) for a task.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 22,\n   \"metadata\": {\n    \"collapsed\": false,\n    \"scrolled\": true\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/task/timeout/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u'time limits set successfully'}\"\n      ]\n     },\n     \"execution_count\": 22,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/timeout/{}'.format(task_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'taskname': 'tasks.add', 'hard': '3.14', 'soft': '3'})  # You can omit soft or hard\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Worker API\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 55,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Once for the documentation\\n\",\n    \"worker_api = '{}/worker'.format(api_root)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## workers\\n\",\n    \"List workers.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 25,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/workers\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'miki-manjaro': {u'completed_tasks': 0,\\n\",\n       \"  u'concurrency': 1,\\n\",\n       \"  u'queues': [u'celery'],\\n\",\n       \"  u'running_tasks': 0,\\n\",\n       \"  u'status': True}}\"\n      ]\n     },\n     \"execution_count\": 25,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/workers'.format(api_root)  # Only one not under /worker\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.get(url)\\n\",\n    \"workers = resp.json()\\n\",\n    \"workers\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## pool/shutdown\\n\",\n    \"Shutdown a worker.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 30,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/shutdown/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u'Shutting down!'}\"\n      ]\n     },\n     \"execution_count\": 30,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"worker = workers.keys()[0]\\n\",\n    \"url = '{}/shutdown/{}'.format(worker_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url)\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## pool/restart\\n\",\n    \"Restart a worker pool, you need to have [CELERYD_POOL_RESTARTS](https://docs.celeryq.dev/en/latest/configuration.html#std:setting-CELERYD_POOL_RESTARTS) enabled in your configuration).\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 43,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/pool/restart/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"Restarting 'miki-manjaro' worker's pool\\\"}\"\n      ]\n     },\n     \"execution_count\": 43,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"pool_api = '{}/pool'.format(worker_api)\\n\",\n    \"url = '{}/restart/{}'.format(pool_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url)\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## pool/grow\\n\",\n    \"Grows worker pool.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 53,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/pool/grow/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"Growing 'miki-manjaro' worker's pool\\\"}\"\n      ]\n     },\n     \"execution_count\": 53,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/grow/{}'.format(pool_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'n': '10'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## pool/shrink\\n\",\n    \"Shrink worker pool.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 54,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/pool/shrink/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"Shrinking 'miki-manjaro' worker's pool\\\"}\"\n      ]\n     },\n     \"execution_count\": 54,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/shrink/{}'.format(pool_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'n': '3'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## pool/autoscale\\n\",\n    \"[Autoscale](https://docs.celeryq.dev/en/latest/userguide/workers.html#autoscaling) a pool.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 58,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/pool/autoscale/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"Autoscaling 'miki-manjaro' worker\\\"}\"\n      ]\n     },\n     \"execution_count\": 58,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/autoscale/{}'.format(pool_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'min': '3', 'max': '10'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## queue/add-consumer\\n\",\n    \"[Add a consumer](https://docs.celeryq.dev/en/latest/userguide/workers.html#std:control-add_consumer) to a queue.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 62,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/queue/add-consumer/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u\\\"add consumer u'jokes'\\\"}\"\n      ]\n     },\n     \"execution_count\": 62,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"queue_api = '{}/queue'.format(worker_api)\\n\",\n    \"url = '{}/add-consumer/{}'.format(queue_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'queue': 'jokes'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## queue/cancel-consumer\\n\",\n    \"[Cancel a consumer](https://docs.celeryq.dev/en/latest/userguide/workers.html#queues-cancelling-consumers) queue.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 63,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/worker/queue/cancel-consumer/miki-manjaro\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'message': u'no longer consuming from jokes'}\"\n      ]\n     },\n     \"execution_count\": 63,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/cancel-consumer/{}'.format(queue_api, worker)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.post(url, params={'queue': 'jokes'})\\n\",\n    \"resp.json()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Queue API\\n\",\n    \"\\n\",\n    \"We assume that we've two queues; the default one 'celery' and 'all'\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"http://localhost:5555/api/queues/length\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{u'active_queues': [{u'messages': 2, u'name': u'all'},\\n\",\n       \"  {u'messages': 1, u'name': u'celery'}]}\"\n      ]\n     },\n     \"execution_count\": 7,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"url = '{}/queues/length'.format(api_root)\\n\",\n    \"print(url)\\n\",\n    \"resp = requests.get(url)\\n\",\n    \"resp.json()\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 2\",\n   \"language\": \"python\",\n   \"name\": \"python2\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 2\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython2\",\n   \"version\": \"2.7.6\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "docs/api.rst",
    "content": "API Reference\n=============\n\nFor security reasons, the Flower API is disabled by default when authentication is not enabled.\nTo enable the API for unauthenticated environments, you can set the FLOWER_UNAUTHENTICATED_API\nenvironment variable to true.\n\n.. autotornado:: flower.app:Flower()\n"
  },
  {
    "path": "docs/auth.rst",
    "content": ".. _authentication:\n\nAuthentication\n==============\n\nFlower supports a variety of authentication methods, including Basic Authentication, Google, GitHub,\nGitLab, and Okta OAuth. You can also customize and use your own authentication method.\n\nThe following endpoints are exempt from authentication:\n\n- /healthcheck\n- /metrics\n\n.. _basic-authentication:\n\nHTTP Basic Authentication\n-------------------------\n\nFlower supports Basic Authentication as a built-in authentication method, allowing you to secure access to the\nFlower using simple username and password credentials. This authentication method is commonly used for\nstraightforward authentication requirements.\n\nTo enable basic authentication, use :ref:`basic_auth` option. This option allows you to specify a list of\nusername and password pairs for authentication.\n\nFor example, running Flower with the following :ref:`basic_auth` option will protect the Flower UI and\nonly allow access to users providing the username user and the password pswd::\n\n    $ celery flower --basic-auth=user:pswd\n\nSee also :ref:`reverse-proxy`\n\n.. _google-oauth:\n\nGoogle OAuth\n------------\n\nFlower provides authentication support using Google OAuth, enabling you to authenticate users through their Google accounts.\nThis integration simplifies the authentication process and offers a seamless experience for users who are already logged into Google.\n\nFollow the steps below to configure and use Google OAuth authentication:\n\n1. Go to the `Google Developer Console`_\n2. Select a project, or create a new one.\n3. In the sidebar on the left, select Credentials.\n4. Click CREATE CREDENTIALS and click OAuth client ID.\n5. Under Application type, select Web application.\n6. Name OAuth 2.0 client and click Create.\n7. Copy the \"Client secret\" and \"Client ID\"\n8. Add redirect URI to the list of Authorized redirect URIs\n\nHere's an example configuration file with the Google OAuth options:\n\n.. code-block:: python\n\n    auth_provider=\"flower.views.auth.GoogleAuth2LoginHandler\"\n    auth=\"allowed-emails.*@gmail.com\"\n    oauth2_key=\"<your_client_id>\"\n    oauth2_secret=\"<your_client_secret>\"\n    oauth2_redirect_uri=\"http://localhost:5555/login\"\n\nReplace `<your_client_id>` and `<your_client_secret>` with the actual  Client ID and secret obtained from\nthe Google Developer Console.\n\n.. _Google Developer Console: https://console.developers.google.com\n\n.. _github-oauth:\n\nGitHub OAuth\n------------\n\nFlower also supports GitHub OAuth. Before getting started, Flower should be registered in\n`Github Settings`_.\n\nGithub OAuth is activated by setting :ref:`auth_provider` to `flower.views.auth.GithubLoginHandler`.\nHere's an example configuration file with the Github OAuth options:\n\n.. code-block:: python\n\n    auth_provider=\"flower.views.auth.GithubLoginHandler\"\n    auth=\"allowed-emails.*@gmail.com\"\n    oauth2_key=\"<your_client_id>\"\n    oauth2_secret=\"<your_client_secret>\"\n    oauth2_redirect_uri=\"http://localhost:5555/login\"\n\nReplace `<your_client_id>` and `<your_client_secret>` with the actual  Client ID and secret obtained from\nthe Github Settings.\n\nSee `GitHub OAuth API`_ docs for more info.\n\n.. _Github Settings: https://github.com/settings/applications/new\n.. _GitHub OAuth API: https://developer.github.com/v3/oauth/\n\n.. _okta-oauth:\n\nOkta OAuth\n----------\n\nFlower also supports Okta OAuth. Before getting started, you need to register Flower in `Okta`_.\nOkta OAuth is activated by setting :ref:`auth_provider` option to `flower.views.auth.OktaLoginHandler`.\n\nOkta OAuth requires `oauth2_key`, `oauth2_secret` and `oauth2_redirect_uri` options which should be obtained from Okta.\nOkta OAuth also uses `FLOWER_OAUTH2_OKTA_BASE_URL` environment variable.\n\nSee Okta `Okta OAuth API`_ docs for more info.\n\n.. _Okta: https://developer.okta.com/docs/guides/add-an-external-idp/openidconnect/main/\n.. _Okta OAuth API: https://developer.okta.com/docs/reference/api/oidc/\n\n.. _gitlab-oauth:\n\nGitLab OAuth\n------------\n\nFlower also supports GitLab OAuth for authentication. To enable GitLab OAuth, follow the steps below:\n\n1. Register Flower as an application at GitLab. You can refer to the `GitLab OAuth documentation`_ for detailed instructions on how to do this.\n2. Once registered, you will obtain the credentials for Flower configuration.\n3. In your Flower configuration, set the following options to activate GitLab OAuth:\n    - :ref:`auth_provider` to `flower.views.auth.GitLabLoginHandler`.\n    - :ref:`oauth2_key` to the \"Application ID\" obtained from GitLab.\n    - :ref:`oauth2_secret` to the \"Secret\" obtained from GitLab.\n    - :ref:`oauth2_redirect_uri`: Set this to the redirect URI configured in GitLab.\n4. (Optional) To restrict access to specific GitLab groups, you can utilize the `FLOWER_GITLAB_AUTH_ALLOWED_GROUPS` environment variable. Set it to a comma-separated list of allowed groups. You can include subgroups by using the `/` character. For example: `group1,group2/subgroup`.\n5. (Optional) The default minimum required group access level can be adjusted using the `FLOWER_GITLAB_MIN_ACCESS_LEVEL` environment variable.\n6. (Optional) The custom GitHub Domain can be adjusted using the `FLOWER_GITLAB_OAUTH_DOMAIN` environment variable.\n\nFor further details on GitLab OAuth and its implementation, refer to the `Group and project members API`_ documentation.\nIt provides comprehensive information and guidelines on working with GitLab's OAuth functionality.\n\nSee also `GitLab OAuth2 API`_ documentation for more info.\n\n.. _GitLab OAuth documentation: https://docs.gitlab.com/ee/integration/oauth_provider.htm\n.. _GitLab OAuth2 API: https://docs.gitlab.com/ee/api/oauth2.html\n.. _Group and project members API: https://docs.gitlab.com/ee/api/members.html\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# flower documentation build configuration file, created by\n# sphinx-quickstart on Fri Apr 11 17:26:01 2014.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\nimport sys\nimport os\n\nsys.path.insert(0, os.path.abspath('..'))\nimport flower  # noqa: E402\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n# sys.path.insert(0, os.path.abspath('.'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.intersphinx',\n    'sphinxcontrib.httpdomain',\n    'sphinxcontrib.autohttp.tornado',\n    'sphinxcontrib.redoc',\n]\n\ntemplates_path = ['_templates']\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The encoding of source files.\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'Flower'\ncopyright = '2023, Mher Movsisyan'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '.'.join(map(str, flower.VERSION[0:2]))\n# The full version, including alpha/beta/rc tags.\nrelease = flower.__version__.rstrip('-dev')\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n# language = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n# today = ''\n# Else, today_fmt is used as the format for a strftime call.\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = ['.build']\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = 'celery'\nhtml_theme_path = ['_theme']\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n# html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n# html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n# html_logo = None\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n# html_extra_path = []\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\n# html_last_updated_fmt = '%b %d, %Y'\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\nhtml_sidebars = {\n    'index':    ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],\n    '**':       ['sidebarlogo.html', 'localtoc.html', 'relations.html',\n                 'sourcelink.html', 'searchbox.html']\n}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n# html_additional_pages = {}\n\n# If false, no module index is generated.\nhtml_domain_indices = False\n\n# If false, no index is generated.\nhtml_use_index = False\n\n# If true, the index is split into individual pages for each letter.\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\nhtml_show_sourcelink = False\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\nhtml_show_sphinx = False\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'flowerdoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    # ' pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    # 'preamble': '',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (\n        'index',\n        'flower.tex',\n        'flower Documentation',\n        'Mher Movsisyan',\n        'manual'\n    ),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n# latex_appendices = []\n\n# If false, no module index is generated.\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    ('man', 'flower', 'flower Documentation',\n     ['Mher Movsisyan'], 1)\n]\n\n# If true, show URL addresses after external links.\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (\n        'index',\n        'flower',\n        'flower Documentation',\n        'Mher Movsisyan',\n        'flower',\n        'One line description of project.',\n        'Miscellaneous'\n    ),\n]\n\n# Documents to append as an appendix to all manuals.\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n# texinfo_no_detailmenu = False\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {'http://docs.python.org/': None}\n\n"
  },
  {
    "path": "docs/config.rst",
    "content": ":tocdepth: 2\n\nConfiguration\n=============\n\nFlower is highly customizable. You can pass configuration options through the command line,\nconfiguration file, or environment variables. For a full list of options, see the `Option Reference`_ section.\n\nCommand line\n------------\n\nFlower operates as a sub-command of Celery, allowing you to pass both Celery and Flower\nconfiguration options from the command line. The template for this is as follows ::\n\n    celery [celery options] flower [flower options]\n\nCelery options should be specified after the celery command, while Flower options\nshould be specified after the flower sub-command.\n\nFor example ::\n\n    $ celery --broker=redis:// flower --unix-socket=/tmp/flower.sock\n\nSee `Celery Configuration reference`_ for a comprehensive listing of all available settings\nand their default values.\n\n.. _`Celery Configuration reference`: https://docs.celeryq.dev/en/latest/userguide/configuration.html\n\nConfiguration file\n------------------\n\nFlower tries to load configuration from the :file:`flowerconfig.py` file by default.\nYou can override the name of the configuration file with the `conf`_ option.\nThe configuration file is a simple Python file that contains key-value pairs:\n\n.. code-block:: python\n\n    # Set RabbitMQ management api\n    broker_api = 'http://guest:guest@localhost:15672/api/'\n\n    # Enable debug logging\n    logging = 'DEBUG'\n\nEnvironment variables\n---------------------\n\nFlower configuration options can also be passed through environment variables.\nAll Flower options must be prefixed with `FLOWER_`.\nFor example, to set the basic_auth option to foo:bar, you would set the\n`FLOWER_BASIC_AUTH` environment variable to `foo:bar` ::\n\n    export FLOWER_BASIC_AUTH=foo:bar\n    celery flower\n\n.. _options_referance:\n\nOption Reference\n-----------------\n\n.. contents::\n    :local:\n    :depth: 1\n\n.. _address:\n\naddress\n~~~~~~~\n\nDefault: '' (empty string)\n\nSets the address on which the Flower HTTP server should listen.\nThe address may be either an IP address or a hostname. If a hostname is provided,\nthe server will listen on all IP addresses associated with that name.\nTo listen on all available interfaces, set the address to an empty string.\n\nExample:\n\nListen on all available interfaces::\n\n    $ celery flower --address='0.0.0.0'\n\nListen only on the loopback interface::\n\n    $ celery flower --address='localhost'\n\nListen on all IP addresses associated with 'example.com'::\n\n    $ celery flower --address='example.com'\n\n\n.. _auth:\n\nauth\n~~~~\n\nDefault: '' (empty string)\n\nEnables authentication. `auth` is a regular expression of emails to grant access.\n\nThe `auth` option allows you to enable authentication in Flower. By default, the `auth` option is set to an empty string, indicating that authentication is disabled.\n\nTo enable authentication and restrict access to specific email addresses, set the `auth` option to a regular expression pattern that matches the desired email addresses. The `auth` option supports a basic regex syntax, including:\n\n  - Single email: Use a single email address, such as `user@example.com`.\n  - Wildcard: Use a wildcard pattern with `.*` to match multiple email addresses with the same domain, such as `.*@example.com`.\n  - List of emails: Use a list of emails separated by pipes (`|`), such as `one@example.com|two@example.com`.\n\nPlease note that for security reasons, the `auth` option only supports a basic regex syntax and does not provide advanced regex features.\n\nFor more information and detailed usage examples, refer to the :ref:`Authentication` section of the Flower documentation.\n\n.. _auto_refresh:\n\nauto_refresh\n~~~~~~~~~~~~\n\nDefault: True\n\nEnables automatic refresh for the Workers view.\nBy default, the Workers view automatically refreshes at regular intervals to provide up-to-date\ninformation about the workers. Set this option to `False` to disable automatic refreshing.\n\n.. _basic_auth:\n\nbasic_auth\n~~~~~~~~~~\n\nDefault: None\n\nEnables HTTP Basic authentication. It accepts a comma-separated list of `username:password` pairs.\nEach pair represents a valid username and password combination for authentication.\n\nExample:\n\nEnable HTTP Basic authentication with multiple users::\n\n    $ celery flower --basic-auth=\"user1:password1,user2:password2\"\n\nSee :ref:`basic-authentication` for more information.\n\n.. _broker_api:\n\nbroker_api\n~~~~~~~~~~\n\nDefault: None\n\nThe URL of the broker API used by Flower to retrieve information about queues.\n\nFlower uses the RabbitMQ Management Plugin to gather information about queues.\nThe `broker_api` option should be set to the URL of the RabbitMQ HTTP API, including user credentials if required.\n\nExample::\n\n    $ celery flower broker-api=\"http://username:password@rabbitmq-server-name:15672/api/\"\n\n.. Note:: By default, the RabbitMQ Management Plugin is not enabled. To enable it, run the following command::\n\n    $ rabbitmq-plugins enable rabbitmq_management\n\n.. Note:: The port number for RabbitMQ versions prior to 3.0 is 55672.\n\nFor more information refer to the `RabbitMQ Management Plugin`_ documentation.\n\n.. _`RabbitMQ Management Plugin`: https://www.rabbitmq.com/management.html\n\n.. _ca_certs:\n\nca_certs\n~~~~~~~~\n\nDefault: None\n\nSets the path to the `ca_certs` file containing a set of concatenated \"certification authority\" certificates.\n\nThe `ca_certs` file is used to validate certificates received from the other end of the connection.\nIt contains a collection of trusted root certificates. Set the `ca_certs` option to the path of the `ca_certs` file.\nIf not specified, certificate validation will not be performed.\n\nFor more information about certificate validation in Python, refer to the `Python SSL`_ documentation.\n\n.. _`Python SSL`: https://docs.python.org/3/library/ssl.html\n\n.. _certfile:\n\ncertfile\n~~~~~~~~\n\nDefault: None\n\nSets the path to the SSL certificate file.\n\nThe `certfile` option specifies the path to the SSL certificate file used for SSL/TLS encryption.\nThe certificate file contains the public key certificate for the Flower server.\nIf not specified, SSL/TLS encryption will not be used.\n\n.. _conf:\n\nconf\n~~~~\n\nDefault: flowerconfig.py\n\nSets the configuration file to be used by Flower.\n\nExample::\n\n    $ celery flower --conf=\"./examples/celeryconfig.py\"\n\n.. _db:\n\ndb\n~~\n\nDefault: flower\n\nSets the database file to use if persistent mode is enabled.\n\nIf the `persistent`_ mode is enabled, the `db` option specifies the database file\nto be used by Flower for storing task results, events, or other persistent data.\n\nExample::\n\n    $ celery flower --persistent=True --db=\"flower_db\"\n\n.. _debug:\n\ndebug\n~~~~~\n\nDefault: False\n\nEnables the debug mode\n\n.. Note:: When debug mode is enabled, Flower may print sensitive information\n\n.. _enable_events:\n\nenable_events\n~~~~~~~~~~~~~\n\nDefault: True\n\nWhen enabled, Flower periodically sends Celery `enable_events` commands to all workers.\nEnabling Celery events allows Flower to receive real-time updates about task events\nfrom the Celery workers.\n\nYou can also enable events directly when running Celery workers by using the `-E` flag.\nFor more information, refer to the `Celery documentation <https://docs.celeryq.dev/en/stable/reference/cli.html#cmdoption-celery-worker-E>`_:\n\n.. _format_task:\n\nformat_task\n~~~~~~~~~~~\n\nDefault: None\n\nModifies the default task formatting.\n\nThe `format_task` function allows to modify the default formatting of tasks.\nBy defining the `format_task` function in the `flowerconfig.py` configuration file,\nyou can customize the task object before it is displayed. The `format_task` function accepts a task object\nas a parameter and should return the modified version of the task.\n\nThis function is particularly useful for filtering out sensitive information or limiting display lengths of task arguments, kwargs, or results.\n\nThe example below shows how to filter arguments and limit display lengths:\n\n.. code-block:: python\n\n    from flower.utils.template import humanize\n\n    def format_task(task):\n        task.args = humanize(task.args, length=10)\n        task.kwargs.pop('credit_card_number')\n        task.result = humanize(task.result, length=20)\n        return task\n\n.. _inspect_timeout:\n\ninspect_timeout\n~~~~~~~~~~~~~~~\n\nDefault: 1000\n\nSets the timeout for the worker inspect commands in milliseconds.\n\nWorker inspection involves retrieving information about the workers, such as their current status, tasks, and resource usage.\n\n.. _keyfile:\n\nkeyfile\n~~~~~~~\n\nDefault: None\n\nSets the path to the SSL key file.\n\nThe key file contains the private key corresponding to the SSL certificate.\nIf not specified, or set to `None`, SSL/TLS encryption will not be used.\n\n.. _max_workers:\n\nmax_workers\n~~~~~~~~~~~\n\nDefault: 5000\n\nSets the maximum number of workers to keep in memory\n\n.. _max_tasks:\n\nmax_tasks\n~~~~~~~~~\n\nDefault: 100000\n\nSets the maximum number of tasks to keep in memory\n\n.. _natural_time:\n\nnatural_time\n~~~~~~~~~~~~\n\nDefault: False\n\nEnables showing time relative to the page refresh time in a more human-readable format.\n\nWhen enabled, timestamps will be shown as relative time such as \"2 minutes ago\" or \"1 hour ago\" instead of the exact timestamp.\n\n.. _persistent:\n\npersistent\n~~~~~~~~~~\n\nDefault: False\n\nEnables persistent mode in Flower.\n\nWhen persistent mode is enabled, Flower saves its current state and reloads it upon restart.\nThis ensures that Flower retains its state and configuration across restarts.\nFlower stores its state in a database file specified by the `db`_ option.\n\n.. _port:\n\nport\n~~~~\n\nDefault: 5555\n\nSets the port number for running the Flower HTTP server.\n\n.. _state_save_interval:\n\nstate_save_interval\n~~~~~~~~~~~~~~~~~~~\n\nDefault: 0\n\nSets the interval for saving the Flower state.\n\nFlower state includes information about workers, tasks. The state is saved periodically to ensure data persistence and recovery upon restart.\n\nBy default, periodic saving is disabled. Flower will not automatically save its state at regular intervals.\nIf you want to enable periodic state saving, set the `state_save_interval` option to a positive integer value representing the interval in milliseconds.\n\n.. _xheaders:\n\nxheaders\n~~~~~~~~\n\nDefault: False\n\nEnables support for `X-Real-Ip` and `X-Scheme` headers.\n\nThe `xheaders` option allows Flower to enable support for `X-Real-Ip` and `X-Scheme` headers.\nThese headers are commonly used in proxy or load balancer configurations to preserve the original client IP address and scheme.\n\n.. _tasks_columns:\n\ntasks_columns\n~~~~~~~~~~~~~\n\nDefault: name,uuid,state,args,kwargs,result,received,started,runtime,worker\n\nSpecifies the list of comma-delimited columns to display on the `/tasks` page.\n\nThe `tasks_columns` option allows you to customize the columns displayed on the `/tasks` page in Flower.\nBy default, the specified columns are: name, uuid, state, args, kwargs, result, received, started, runtime, and worker.\n\nAvailable columns are:\n\n  - `name`\n  - `uuid`\n  - `state`\n  - `args`\n  - `kwargs`\n  - `result`\n  - `received`\n  - `started`\n  - `runtime`\n  - `worker`\n  - `retries`\n  - `revoked`\n  - `exception`\n  - `expires`\n  - `eta`\n\nExample::\n\n    $ celery flower --tasks-columns='name,uuid,state,args,kwargs,result,received,started,runtime,worker,retries,revoked,exception,expires,eta'\n\nIn the above example, all available columns are displayed.\n\n.. _url_prefix:\n\nurl_prefix\n~~~~~~~~~~\n\nDefault: '' (empty string)\n\nEnables deploying Flower on a non-root URL.\n\nThe `url_prefix` option allows you to deploy Flower on a non-root URL.\nBy default, Flower is deployed on the root URL. However, if you need to run Flower on a specific path,\nsuch as `http://example.com/flower`, you can specify the desired URL prefix using the `url_prefix` option.\n\n.. _unix_socket:\n\nunix_socket\n~~~~~~~~~~~\n\nDefault: '' (empty string)\n\nRuns Flower using a UNIX socket file.\n\nThe `unix_socket` option allows you to run Flower using a UNIX socket file instead of a network port.\nBy default, the `unix_socket` option is set to an empty string, indicating that Flower should not use a UNIX socket.\n\nTo run Flower using a UNIX socket file, set the `unix_socket` option to the desired path of the UNIX socket file.\nFlower will then bind to the specified socket file instead of a network port.\n\nExample::\n\n    $ celery flower --unix-socket='/var/run/flower.sock'\n\n.. _cookie_secret:\n\ncookie_secret\n~~~~~~~~~~~~~\n\nDefault: token_urlsafe(64) (random string)\n\nSets a secret key for signing cookies.\n\nThe `cookie_secret` option allows you to set a secret key used for signing cookies in Flower.\n\nBy default, the `cookie_secret` option is set to 'token_urlsafe(64)', which generates a random string of length 64 characters as the secret key.\nThis provides a good level of security for signing cookies. If you want to specify a custom secret key, you can set the `cookie_secret` option to the desired string.\n\n.. _auth_provider:\n\nauth_provider\n~~~~~~~~~~~~~\n\nDefault: None\n\nSets the authentication provider for Flower.\n\nThe `auth_provider` option allows you to set the authentication provider for Flower.\nBy default, the `auth_provider` option is set to `None`, indicating that no authentication provider is configured.\n\nTo enable authentication and specify an authentication provider, set the `auth_provider` option to one of the following values:\n\n  - Google `flower.views.auth.GoogleAuth2LoginHandler`\n  - GitHub `flower.views.auth.GithubLoginHandler`\n  - GitLab `flower.views.auth.GitLabLoginHandler`\n  - Okta `flower.views.auth.OktaLoginHandler`\n\nSee also :ref:`Authentication` for usage examples\n\n.. _purge_offline_workers:\n\npurge_offline_workers\n~~~~~~~~~~~~~~~~~~~~~\n\nDefault: None\n\nTime (in seconds) after which offline workers are automatically removed from the Workers view.\nBy default, offline workers will remain on the dashboard indefinitely.\n\n.. _task_runtime_metric_buckets:\n\ntask_runtime_metric_buckets\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDefault: 'Histogram.DEFAULT_BUCKETS' (default prometheus buckets)\n\nSets the task runtime latency buckets.\n\nYou can provide the `buckets` value as a comma-separated list of values.\n\nExample::\n\n    $ celery flower --task-runtime-metric-buckets=1,5,10,inf\n\nThe buckets represent the upper bounds of the latency intervals.\nYou can specify them as integer or float values. The `inf` value represents positive infinity, indicating\nthat the last bucket captures all values greater than or equal to the previous bucket.\n\n.. _oauth2_key:\n\noauth2_key\n~~~~~~~~~~\n\nDefault: None\n\nSets the OAuth 2.0 key (client ID) issued by the OAuth 2.0 provider\n\n`oauth2_key` option should be used with :ref:`auth`, :ref:`auth_provider`, :ref:`oauth2_redirect_uri` and :ref:`oauth2_secret` options.\n\n.. _oauth2_secret:\n\noauth2_secret\n~~~~~~~~~~~~~\n\nDefault: None\n\nSets the OAuth 2.0 secret issued by the OAuth 2.0 provider\n\n`oauth2_secret` option should be used with :ref:`auth`, :ref:`auth_provider`, :ref:`oauth2_redirect_uri` and :ref:`oauth2_key` options.\n\n.. _oauth2_redirect_uri:\n\noauth2_redirect_uri\n~~~~~~~~~~~~~~~~~~~\n\nDefault: None\n\nSets the URI to which an OAuth 2.0 server redirects the user after successful authentication and authorization.\n\n`oauth2_redirect_uri` option should be used with :ref:`auth`, :ref:`auth_provider`, :ref:`oauth2_key` and :ref:`oauth2_secret` options.\n"
  },
  {
    "path": "docs/features.rst",
    "content": "Features\n--------\n\n- Real-time monitoring using Celery Events\n    - View task progress and history\n    - View task details (arguments, start time, runtime, and more)\n\n- Remote Control\n    - View worker status and statistics\n    - Shutdown and restart worker instances\n    - Control worker pool size and autoscale settings\n    - View and modify the queues a worker instance consumes from\n    - View currently running tasks\n    - View scheduled tasks (ETA/countdown)\n    - View reserved and revoked tasks\n    - Apply time and rate limits\n    - Revoke or terminate tasks\n\n- Broker monitoring\n    - View statistics for all Celery queues\n\n- HTTP Basic Auth, Google, Github, Gitlab and Okta OAuth\n\n- Prometheus integration\n\n- API\n"
  },
  {
    "path": "docs/index.rst",
    "content": "======\nFlower\n======\n\nFlower is an open-source web application for monitoring and managing `Celery`_ clusters.\nIt provides real-time information about the status of Celery workers and tasks.\n\n.. _Celery: https://docs.celeryq.dev/en/stable/#\n\nContents\n========\n\n.. toctree::\n\n   features\n   install\n   config\n   tasks_filter\n   auth\n   reverse-proxy\n   prometheus-integration\n   api\n\nFlower is licensed under BSD 3-Clause License.\nSee the `License`_ file for the full license text.\n\nFlower development and support is provided through its GitHub `repository`_.\n\n.. _`License`: https://github.com/mher/flower/blob/master/LICENSE\n.. _`repository`: https://github.com/mher/flower\n"
  },
  {
    "path": "docs/install.rst",
    "content": "Getting started\n===============\n\nInstallation\n------------\n\nInstalling `flower` with `pip <http://www.pip-installer.org/>`_ is simple ::\n\n    $ pip install flower\n\nThe development version can be installed from Github ::\n\n    $ pip install https://github.com/mher/flower/zipball/master#egg=flower\n\nUsage\n-----\n\nTo run Flower, you need to provide the broker URL ::\n\n    $ celery --broker=amqp://guest:guest@localhost:5672// flower\n\nOr use the configuration of `celery application <https://docs.celeryq.dev/en/stable/userguide/application.html>`_  ::\n\n    $ celery -A tasks.app flower\n\nBy default, flower runs on port 5555, which can be modified with the :ref:`port` option ::\n\n    $ celery -A tasks.app flower --port=5001\n\nYou can also run Flower using the docker image ::\n\n    $ docker run -v examples:/data -p 5555:5555 mher/flower celery --app=tasks.app flower\n\nIn this example, Flower is using the `tasks.app` defined in the `examples/tasks.py <https://github.com/mher/flower/blob/master/examples/tasks.py>`_ file\n"
  },
  {
    "path": "docs/man.rst",
    "content": "========\n flower\n========\n\nSYNOPSIS\n========\n\n``flower`` [*OPTIONS*]\n\nDESCRIPTION\n===========\n\nFlower is a web based tool for monitoring and administrating Celery clusters.\nIt has these features:\n\n- Real-time monitoring using Celery Events\n\n    - Task progress and history\n    - Ability to show task details (arguments, start time, runtime, and more)\n    - Graphs and statistics\n\n- Remote Control\n\n    - View worker status and statistics\n    - Shutdown and restart worker instances\n    - Control worker pool size and autoscale settings\n    - View and modify the queues a worker instance consumes from\n    - View currently running tasks\n    - View scheduled tasks (ETA/countdown)\n    - View reserved and revoked tasks\n    - Apply time and rate limits\n    - Configuration viewer\n    - Revoke or terminate tasks\n\n- Broker monitoring\n\n    - View statistics for all Celery queues\n    - Queue length graphs\n\n- HTTP API\n- Basic Auth and Google OpenID authentication\n- Prometheus integration\n\n\nOPTIONS\n=======\n\n  --address                        run on the given address\n  --auth                           regexp  of emails to grant access\n  --auth_provider                  sets authentication provider class\n  --auto_refresh                   refresh workers automatically (default *True*)\n  --basic_auth                     colon separated user-password to enable\n                                   basic auth\n  --broker_api                     inspect broker e.g.\n                                   http://guest:guest@localhost:15672/api/\n  --ca_certs                       path to SSL certificate authority (CA) file\n  --certfile                       path to SSL certificate file\n  --conf                           flower configuration file path (default *flowerconfig.py*)\n  --cookie_secret                  secure cookie secret\n  --db                             flower database file (default *flower*)\n  --debug                          run in debug mode (default *False*)\n  --enable_events                  periodically enable Celery events (default *True*)\n  --format_task                    use custom task formatter\n  --help                           show this help information\n  --inspect                        inspect workers (default *True*)\n  --inspect_timeout                inspect timeout (in milliseconds) (default\n                                   *1000*)\n  --keyfile                        path to SSL key file\n  --max_workers                     maximum number of workers to keep in memory\n                                   (default *5000*)\n  --max_tasks                      maximum number of tasks to keep in memory\n                                   (default *10000*)\n  --natural_time                   show time in relative format (default *False*)\n  --persistent                     enable persistent mode (default *False*)\n  --port                           run on the given port (default *5555*)\n  --purge_offline_workers          time (in seconds) after which offline workers are purged\n                                   from workers\n  --state_save_interval            state save interval (in milliseconds) (default *0*)\n  --tasks_columns                  slugs of columns on /tasks/ page, delimited by comma\n                                   (default *name,uuid,state,args,kwargs,result,received,started,runtime,worker*)\n  --unix_socket                    path to unix socket to bind flower server to\n  --url_prefix                     base url prefix\n  --xheaders                       enable support for the 'X-Real-Ip' and\n                                   'X-Scheme' headers. (default *False*)\n  --task_runtime_metric_buckets    task runtime prometheus latency metric buckets (default prometheus latency buckets)\n\nTORNADO OPTIONS\n===============\n\n  --log_file_max_size              max size of log files before rollover\n                                   (default *100000000*)\n  --log_file_num_backups           number of log files to keep (default *10*)\n  --log_file_prefix=PATH           Path prefix for log files. Note that if you\n                                   are running multiple tornado processes,\n                                   log_file_prefix must be different for each\n                                   of them (e.g. include the port number)\n  --log_to_stderr                  Send log output to stderr (colorized if\n                                   possible). By default use stderr if\n                                   ``--log_file_prefix`` is not set and no other\n                                   logging is configured.\n  --logging=debug|info|warning|error|none\n                                   Set the Python log level. If *none*, tornado\n                                   won't touch the logging configuration.\n                                   (default *info*)\n\nUSAGE\n=====\n\nLaunch the Flower server at specified port other than default 5555 (open the UI at http://localhost:5566): ::\n\n    $ celery flower --port=5566\n\nSpecify Celery application path with address and port for Flower: ::\n\n    $ celery -A proj flower --address=127.0.0.6 --port=5566\n\nBroker URL and other configuration options can be passed through the standard Celery options (notice that they are after\nCelery command and before Flower sub-command): ::\n\n    $ celery -A proj --broker=amqp://guest:guest@localhost:5672// flower\n"
  },
  {
    "path": "docs/prometheus-integration.rst",
    "content": "Prometheus Integration\n======================\n\nFlower exports several celery worker and task metrics in Prometheus' format.\nThe ``/metrics`` endpoint is available from the get go after you have installed Flower.\n\nBy default on your local machine Flower's metrics are available at: ``localhost:5555/metrics``.\n\nRead further for more information about configuration and available metrics please.\n\nComplete guide on integration of Celery, Flower, Prometheus and Grafana is here: `Grafana Integration Guide`_.\n\nConfigure Prometheus to scrape Flower metrics\n---------------------------------------------\n\nTo integrate with Prometheus you have to add Flower as the target in Prometheus's configuration.\nIn this example we are assuming your Flower and Prometheus are installed on your local machine\nwith their defaults and available at ``localhost:<port number>``.\n\nTo add Flower's metrics to Prometheus go to its config file ``prometheus.yml`` which initially\nwill look like this:\n\n.. code-block:: yaml\n\n    global:\n      scrape_interval:     15s\n      evaluation_interval: 15s\n\n    scrape_configs:\n      - job_name: prometheus\n        static_configs:\n          - targets: ['localhost:9090']\n\nand alter the ``scrape_configs`` definition to be:\n\n.. code-block:: yaml\n\n    scrape_configs:\n      - job_name: prometheus\n        static_configs:\n          - targets: ['localhost:9090']\n      - job_name: flower\n        static_configs:\n          - targets: ['localhost:5555']\n\nYou can also just point Prometheus at the example ``prometheus.yml`` file in the root of the `Flower repository <https://github.com/mher/flower/prometheus.yml>`\nwhen you start it from the command line (note that you would have to set ``flower`` to point at ``localhost`` in your ``etc/hosts`` config for the DNS to resolve correctly)::\n\n    ./prometheus --config.file=prometheus.yml\n\nAvailable Metrics\n-----------------\n\nBelow you will find a table of available Prometheus metrics exposed by Flower.\n\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| Name                                              | Description                                                          |  Labels            | Instrument Type |\n+===================================================+======================================================================+====================+=================+\n| flower_events_total                               | Number of times a celery task event was registered by Flower.        | task, type, worker | counter         |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| flower_task_prefetch_time_seconds                 | The time the task spent waiting at the celery worker to be executed. | task, worker       | gauge           |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| flower_worker_prefetched_tasks                    | Number of tasks of given type prefetched at a worker.                | task, worker       | gauge           |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| flower_task_runtime_seconds                       | The time it took to run the task.                                    | task, worker       | histogram       |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| flower_worker_online                              | Shows celery worker's online status.                                 | worker             | gauge           |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n| flower_worker_number_of_currently_executing_tasks | Number of tasks currently executing at this worker.                  | worker             | gauge           |\n+---------------------------------------------------+----------------------------------------------------------------------+--------------------+-----------------+\n\nUsing Metric Labels\n-------------------\n\nYou can filter received data in prometheus using ``promql`` syntax to present information only for selected labels.\nWe have the following labels available:\n\n* **task** - task name, i.e. ``tasks.add``, ``tasks.multiply``.\n* **type** - task event type, i.e. ``task-started``, ``task-succeeded``. Note that worker related events **will not be counted**.\n  For more info on task event types see: `task events in celery <https://docs.celeryq.dev/en/stable/userguide/monitoring.html#task-events>`_.\n* **worker** - celery worker name, i.e ``celery@<your computer name>``.\n\nExample Prometheus Alerts\n-------------------------\n\nSee example `Prometheus alerts <https://github.com/mher/flower/tree/master/examples/prometheus-alerts.yaml>`_.\nAdd the rules to your ``alertmanager.yml`` config as in the `alert manager's documentation <https://prometheus.io/docs/alerting/latest/configuration/>`_.\n\n\nExample Grafana Dashboard\n-------------------------\n\nSee example `Grafana dashboard <https://github.com/mher/flower/tree/master/examples/celery-monitoring-grafana-dashboard.json>`_.\nYou can import it easily in Grafana.\nHover over the + button in the side bar menu -> Import -> Upload JSON file.\nThe dashboard should give you a nice starting point for monitoring of your celery cluster.\n\nGrafana Integration Guide\n=========================\n\nIn this guide you will learn how to setup each part of the stack to make it talk to the next one and achieve Celery\nmonitoring solution with help of Flower.\n\nSame as above we assume localhost usage and for ease of deployment I will use Pycharm configurations to start docker\ncontainers with necessary images. If you do not have docker installed on your system: `download and install it please <https://www.docker.com/get-started>`_.\n\nStart Celery Broker\n-------------------\n\nEasiest is to use `Redis Pycharm run configuration <https://github.com/mher/flower/tree/master/examples/pycharm-configurations/Redis.run.xml>`_.\n\nOr run::\n\n    docker run --name redis -d -p 6379:6379 redis\n\n\nSet Up Your Celery Application\n-------------------------------\n\nWe are assuming that your Celery application has tasks in `tasks.py` file. The `-E` argument makes Celery send events\nwhich are required to produce Prometheus metrics.\n\nCreate `celeryconfig.py` in root of your Celery app. We are setting Celery to use Redis DB as the broker/backend in this\nexample. Skip this if you configure your broker/backend already in another way (make sure to adjust further steps to that).\n\n.. code-block:: python\n\n    broker_url = 'redis://localhost:6379/0'\n    celery_result_backend = 'redis://localhost:6379/0'\n\nOr download it from `here <https://github.com/mher/flower/tree/master/examples/celeryconfig.py>`_.\n\nStart your Celery app::\n\n    celery -A tasks worker -l INFO -E\n\nWhen the app starts you should see this line::\n\n    -- ******* ---- .> task events: ON\n\n\nStart Flower Monitoring\n-----------------------\n\nIn your Celery application folder run this command (Flower needs to be installed)::\n\n    celery -A tasks --broker=redis://localhost:6379/0 flower\n\nConfigure and Start Prometheus\n------------------------------\n\nCreate `prometheus.yml` file. Note its absolute path - we will use it to start the Prometheus docker image.\nFor ease of use put it in the root of your Celery project (so that you can use Pycharm configuration below without any changes).\n\n.. code-block:: yaml\n\n    global:\n      scrape_interval:     15s\n      evaluation_interval: 15s\n\n    scrape_configs:\n      - job_name: prometheus\n        static_configs:\n          - targets: ['localhost:9090']\n      - job_name: flower\n        static_configs:\n          - targets: ['localhost:5555']\n\nRun Prometheus inside docker:\n\nYou can use `Prometheus Pycharm run configuration <https://github.com/mher/flower/tree/master/examples/pycharm-configurations/Prometheus.run.xml>`_ (may need to adjust the `prometheus.yml` path if it is not in root of your Celery project).\n\nOr just start it via command line::\n\n    docker run --name Prometheus -v <ABSOLUTE PATH TO YOUR prometheus.yml FILE>:/etc/prometheus/prometheus.yml -p 9090:9090 --network host prom/prometheus\n\n\nNow go to `localhost:9090` and check that Prometheus is running.\nIf everything so far was set up and started correctly, you should be able to see metrics provided by Flower in your\nPrometheus's GUI. Go to `Graph` tab and start typing `flower` - the autocomplete should show you all available metrics.\n\n.. image:: screenshots/flower-metrics-in-prometheus.png\n   :width: 100%\n\nStart Grafana\n-------------\n\nYou can use `Grafana Pycharm run configuration <https://github.com/mher/flower/tree/master/examples/pycharm-configurations/Grafana.run.xml>`_.\n\nOr run it from the terminal::\n\n    docker run --name Grafana -d -v grafana-storage:/var/lib/grafana -p 3000:3000 --network host grafana/grafana\n\ntry to access its web GUI now by going to `localhost:3000`, use `admin/admin` for credentials. It will ask you to set up\na new password - you may click skip for now.\n\n\nAdd Prometheus As a Data Source In Grafana\n------------------------------------------\n\nClick `Configuration` (settings icon) in the left side-bar. Then the blue `Add data source` button.\n\n.. image:: screenshots/grafana-add-data-source.png\n   :width: 100%\n\nSearch for Prometheus data source and click it (it should be at the top).\n\n.. image:: screenshots/grafana-add-prometheus-data-source.png\n   :width: 100%\n\nOnce in Prometheus data source configuration, use all defaults and enter the HTTP/URL parameter as below (which is the placeholder by the way)::\n\n    http://localhost:9090\n\n.. image:: screenshots/grafana-configure-prometheus-data-source.png\n   :width: 100%\n\nScroll down and click `Save & Test`, if all is good a green banner will pop up saying `Data source is working`\n\n.. image:: screenshots/grafana-test-prometheus-data-source.png\n   :width: 100%\n\n\nImport The Celery Monitoring Dashboard In Grafana\n-------------------------------------------------\n\nDownload `Grafana dashboard <https://github.com/mher/flower/tree/master/examples/celery-monitoring-grafana-dashboard.json>`_.\n\nHover over the `+` icon in the left side-bar and click `Import` button.\n\n.. image:: screenshots/grafana-import-dashboard.png\n   :width: 30%\n\nClick `Upload JSON file` button and select the `celery-monitoring-grafana-dashboard.json` you have just downloaded.\n\n.. image:: screenshots/grafana-import-celery-monitoring-dashboard.png\n   :width: 100%\n\nClick on the `Prometheus` field and select a Prometheus data source.\n\n.. image:: screenshots/grafana-configure-imported-dashboard.png\n   :width: 100%\n\nClick `Import` to finish the process.\n\nYou should see a dashboard as on the image below. Congratulations!\n\n.. image:: screenshots/grafana-dashboard.png\n   :width: 100%\n"
  },
  {
    "path": "docs/reverse-proxy.rst",
    "content": ".. _reverse-proxy:\n\nRunning behind reverse proxy\n============================\n\nTo run `Flower` behind a reverse proxy, remember to set the correct `Host` \nheader to the request to make sure Flower can generate correct URLs.\n\nThe following block represents the minimal `nginx` configuration:\n\n.. code-block:: nginx\n\n    server {\n        listen 80;\n        server_name flower.example.com;\n\n        location / {\n            proxy_pass http://localhost:5555;\n        }\n    }\n\nIf you run Flower behind custom location, make sure :ref:`url_prefix` option\nvalue equals to the location path.\n\nYou have to use either environment variable `FLOWER_URL_PREFIX=flower`\nor command parameter `--url_prefix=flower` when you run it\nvia `celery`. With that being set you need the following `nginx` configuration:\n\n.. code-block:: nginx\n\n    server {\n        listen 80;\n        server_name example.com;\n\n        location /flower/ {\n            proxy_pass http://localhost:5555/flower/;\n        }\n    }\n\nwithout `url_prefix` Flower frontend won't be able to generate\ncorrect static links, and without `/flower/` at the end of `proxy_pass`\nparameter, the browser will lead you to 404.\n\nNote that you should not expose this site to the public internet without\nany sort of authentication! If you have a `htpasswd` file with user\ncredentials you can make `nginx` use this file by adding the following\nlines to the location block:\n\n.. code-block:: nginx\n\n    auth_basic \"Restricted\";\n    auth_basic_user_file htpasswd;\n"
  },
  {
    "path": "docs/tasks.py",
    "content": "from celery import Celery\nfrom time import sleep\n\ncelery = Celery()\ncelery.config_from_object({\n    'BROKER_URL': 'amqp://10.0.2.2',\n    'CELERY_RESULT_BACKEND': 'amqp://',\n    'CELERYD_POOL_RESTARTS': True,\n})\n\n\n@celery.task\ndef add(x, y):\n    return x + y\n\n\n@celery.task\ndef sub(x, y):\n    sleep(30)  # Simulate work\n    return x - y\n"
  },
  {
    "path": "docs/tasks_filter.rst",
    "content": "Tasks filtering\n===============\n\nBy now, tasks can be filtered by worker, type, state, received and started datetime.\nAlso, filtering by args/kwargs/result/state value available.\n\nFlower uses github-style syntax for args/kwargs/result filtering.\n\n - `foo` find all tasks containing foo in args, kwargs or result\n - `args:foo` find all tasks containing foo in arguments\n - `kwargs:foo=bar` find all tasks containing foo=bar keyword\n - `result:foo` find all tasks containing foo in result\n - `state:FAILURE` find all failed tasks\n\nIf the search term contains spaces it should be enclosed in \" (e.g. `args:\"hello world\"`).\n\nFor examples, see `tests/utils/test_search.py`.\n"
  },
  {
    "path": "examples/celery-monitoring-grafana-dashboard.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"7.5.2\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"graph\",\n      \"name\": \"Graph\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"description\": \"Basic celery monitoring example\",\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel shows status of celery workers. 1 = online, 0 = offline.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"hiddenSeries\": false,\n      \"id\": 4,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"flower_worker_online\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ worker }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Celery Worker Status\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:150\",\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": \"1\",\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:151\",\n          \"decimals\": null,\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel shows number of tasks currently executing at worker.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"hiddenSeries\": false,\n      \"id\": 9,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"flower_worker_number_of_currently_executing_tasks\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{worker}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Number of Tasks Currently Executing at Worker\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:79\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:80\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel shows average task runtime at worker by worker and task name.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 8\n      },\n      \"hiddenSeries\": false,\n      \"id\": 11,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(flower_task_runtime_seconds_sum[5m]) / rate(flower_task_runtime_seconds_count[5m])\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{task}}, {{worker}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Average Task Runtime at Worker\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:337\",\n          \"format\": \"s\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:338\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel shows task prefetch time at worker by worker and task name.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 17\n      },\n      \"hiddenSeries\": false,\n      \"id\": 12,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"flower_task_prefetch_time_seconds\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{task}}, {{worker}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Task Prefetch Time at Worker\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:337\",\n          \"format\": \"s\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:338\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel shows number of tasks prefetched at worker by task and worker name.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 26\n      },\n      \"hiddenSeries\": false,\n      \"id\": 10,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"flower_worker_prefetched_tasks\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{task}}, {{worker}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Number of Tasks Prefetched At Worker\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:337\",\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:338\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel presents average task success ratio over time by task name.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 35\n      },\n      \"hiddenSeries\": false,\n      \"id\": 2,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"(sum(avg_over_time(flower_events_total{type=\\\"task-succeeded\\\"}[15m])) by (task)  / sum(avg_over_time(flower_events_total{type=~\\\"task-failed|task-succeeded\\\"}[15m])) by (task)) * 100\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ task }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Task Success Ratio\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:63\",\n          \"format\": \"percent\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:64\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"This panel presents average task failure ratio over time by task name.\",\n      \"fieldConfig\": {\n        \"defaults\": {},\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 35\n      },\n      \"hiddenSeries\": false,\n      \"id\": 7,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": false,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.5.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"(sum(avg_over_time(flower_events_total{type=\\\"task-failed\\\"}[15m])) by (task) / sum(avg_over_time(flower_events_total{type=~\\\"task-failed|task-succeeded\\\"}[15m])) by (task)) * 100\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ task }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Task Failure Ratio\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:63\",\n          \"format\": \"percent\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:64\",\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    }\n  ],\n  \"refresh\": \"10s\",\n  \"schemaVersion\": 27,\n  \"style\": \"dark\",\n  \"tags\": [\n    \"celery\",\n    \"monitoring\",\n    \"flower\"\n  ],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-15m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Celery Monitoring\",\n  \"uid\": \"3OBI1flGz\",\n  \"version\": 9\n}"
  },
  {
    "path": "examples/celeryconfig.py",
    "content": "broker_url = 'redis://localhost:6379/0'\ncelery_result_backend = 'redis://localhost:6379/0'\ntask_send_sent_event = False\n"
  },
  {
    "path": "examples/nginx.conf",
    "content": "server {\n    listen 80;\n\n    # with url_prefix=`flower`\n    location /flower/ {\n        proxy_pass http://localhost:5555;\n    }\n}\n"
  },
  {
    "path": "examples/prometheus-alerts.yaml",
    "content": "- alert: CeleryWorkerOffline\n  expr: flower_worker_online == 0\n  for: 2m\n  labels:\n    severity: critical\n    context: celery-worker\n  annotations:\n    summary: Celery worker offline\n    description: Celery worker {{ $labels.worker }} has been offline for more than 2 minutes.\n\n- alert: TaskFailureRatioTooHigh\n  expr: (sum(avg_over_time(flower_events_total{type=\"task-failed\"}[15m])) by (task) / sum(avg_over_time(flower_events_total{type=~\"task-failed|task-succeeded\"}[15m])) by (task)) * 100 > 1\n  for: 5m\n  labels:\n    severity: critical\n    context: celery-task\n  annotations:\n    summary: Task Failure Ratio Too High.\n    description: Average task failure ratio for task {{ $labels.task }} is {{ $value }}.\n\n- alert: TaskPrefetchTimeTooHigh\n  expr: sum(avg_over_time(flower_task_prefetch_time_seconds[15m])) by (task, worker) > 1\n  for: 5m\n  labels:\n    severity: critical\n    context: celery-task\n  annotations:\n    summary: Average Task Prefetch Time Too High.\n    description: Average task prefetch time at worker for task {{ $labels.task }} and worker {{ $labels.worker }} is {{ $value }}.\n"
  },
  {
    "path": "examples/pycharm-configurations/Grafana.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Grafana\" type=\"docker-deploy\" factoryName=\"docker-image\" server-name=\"Docker\">\n    <deployment type=\"docker-image\">\n      <settings>\n        <option name=\"imageTag\" value=\"grafana/grafana\" />\n        <option name=\"command\" value=\"\" />\n        <option name=\"containerName\" value=\"Grafana\" />\n        <option name=\"entrypoint\" value=\"\" />\n        <option name=\"commandLineOptions\" value=\"-d -p 3000:3000 --network=host -v grafana-storage:/var/lib/grafana\" />\n      </settings>\n    </deployment>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": "examples/pycharm-configurations/Prometheus.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Prometheus\" type=\"docker-deploy\" factoryName=\"docker-image\" server-name=\"Docker\">\n    <deployment type=\"docker-image\">\n      <settings>\n        <option name=\"imageTag\" value=\"prom/prometheus\" />\n        <option name=\"command\" value=\"\" />\n        <option name=\"containerName\" value=\"Prometheus\" />\n        <option name=\"entrypoint\" value=\"\" />\n        <option name=\"commandLineOptions\" value=\"-p 9090:9090 -v $PROJECT_DIR$/prometheus.yml:/etc/prometheus/prometheus.yml --network=host\" />\n      </settings>\n    </deployment>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": "examples/pycharm-configurations/Redis.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Redis\" type=\"docker-deploy\" factoryName=\"docker-image\" server-name=\"Docker\">\n    <deployment type=\"docker-image\">\n      <settings>\n        <option name=\"imageTag\" value=\"redis\" />\n        <option name=\"command\" value=\"\" />\n        <option name=\"containerName\" value=\"redis\" />\n        <option name=\"entrypoint\" value=\"\" />\n        <option name=\"commandLineOptions\" value=\"-d -p 6379:6379\" />\n      </settings>\n    </deployment>\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": "examples/tasks.py",
    "content": "import os\nimport time\nfrom datetime import datetime\n\nfrom celery import Celery\n\n\napp = Celery(\"tasks\",\n             broker=os.environ.get('CELERY_BROKER_URL', 'redis://'),\n             backend=os.environ.get('CELERY_RESULT_BACKEND', 'redis'))\napp.conf.accept_content = ['pickle', 'json', 'msgpack', 'yaml']\napp.conf.worker_send_task_events = True\n\n\n@app.task\ndef add(x, y):\n    return x + y\n\n\n@app.task\ndef sleep(seconds):\n    time.sleep(seconds)\n\n\n@app.task\ndef echo(msg, timestamp=False):\n    return \"%s: %s\" % (datetime.now(), msg) if timestamp else msg\n\n\n@app.task\ndef error(msg):\n    raise Exception(msg)\n\n\nif __name__ == \"__main__\":\n    app.start()\n"
  },
  {
    "path": "flower/__init__.py",
    "content": "VERSION = (2, 0, 0)\n__version__ = '.'.join(map(str, VERSION)) + '-dev'\n"
  },
  {
    "path": "flower/__main__.py",
    "content": "import sys\nfrom celery.bin.celery import main as _main, celery\nfrom flower.command import flower\n\n\ndef main():\n    celery.add_command(flower)\n    sys.exit(_main())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "flower/api/__init__.py",
    "content": "import os\n\nimport tornado.web\n\nfrom ..utils import strtobool\nfrom ..views import BaseHandler\n\n\nclass BaseApiHandler(BaseHandler):\n    def prepare(self):\n        enable_api = strtobool(os.environ.get(\n            'FLOWER_UNAUTHENTICATED_API') or \"false\")\n        if not (self.application.options.basic_auth or self.application.options.auth) and not enable_api:\n            raise tornado.web.HTTPError(\n                401, \"FLOWER_UNAUTHENTICATED_API environment variable is required to enable API without authentication\")\n\n    def write_error(self, status_code, **kwargs):\n        exc_info = kwargs.get('exc_info')\n        log_message = exc_info[1].log_message\n        if log_message:\n            self.write(log_message)\n        self.set_status(status_code)\n        self.finish()\n"
  },
  {
    "path": "flower/api/control.py",
    "content": "import logging\n\nfrom tornado import web\n\nfrom . import BaseApiHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass ControlHandler(BaseApiHandler):\n    def is_worker(self, workername):\n        return workername and workername in self.application.workers\n\n    def error_reason(self, workername, response):\n        \"extracts error message from response\"\n        for res in response:\n            try:\n                return res[workername].get('error', 'Unknown reason')\n            except KeyError:\n                pass\n        logger.error(\"Failed to extract error reason from '%s'\", response)\n        return 'Unknown reason'\n\n\nclass WorkerShutDown(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nShut down a worker\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/shutdown/celery@worker2 HTTP/1.1\n  Content-Length: 0\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 29\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Shutting down!\"\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown worker\n        \"\"\"\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        logger.info(\"Shutting down '%s' worker\", workername)\n        self.capp.control.broadcast('shutdown', destination=[workername])\n        self.write(dict(message=\"Shutting down!\"))\n\n\nclass WorkerPoolRestart(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nRestart worker's pool\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/pool/restart/celery@worker2 HTTP/1.1\n  Content-Length: 0\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 56\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Restarting 'celery@worker2' worker's pool\"\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: pool restart is not enabled (see CELERYD_POOL_RESTARTS)\n:statuscode 404: unknown worker\n        \"\"\"\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        logger.info(\"Restarting '%s' worker's pool\", workername)\n        response = self.capp.control.broadcast(\n            'pool_restart', arguments={'reload': False},\n            destination=[workername], reply=True)\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=f\"Restarting '{workername}' worker's pool\"))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to restart the '{workername}' pool: {reason}\")\n\n\nclass WorkerPoolGrow(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nGrow worker's pool\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/pool/grow/celery@worker2?n=3 HTTP/1.1\n  Content-Length: 0\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 58\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Growing 'celery@worker2' worker's pool by 3\"\n  }\n\n:query n: number of pool processes to grow, default is 1\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: failed to grow\n:statuscode 404: unknown worker\n        \"\"\"\n\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        n = self.get_argument('n', default=1, type=int)\n\n        logger.info(\"Growing '%s' worker's pool by '%s'\", workername, n)\n        response = self.capp.control.pool_grow(\n            n=n, reply=True, destination=[workername])\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=f\"Growing '{workername}' worker's pool by {n}\"))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to grow '{workername}' worker's pool: {reason}\")\n\n\nclass WorkerPoolShrink(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nShrink worker's pool\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/pool/shrink/celery@worker2 HTTP/1.1\n  Content-Length: 0\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 60\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Shrinking 'celery@worker2' worker's pool by 1\"\n  }\n\n:query n: number of pool processes to shrink, default is 1\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: failed to shrink\n:statuscode 404: unknown worker\n        \"\"\"\n\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        n = self.get_argument('n', default=1, type=int)\n\n        logger.info(\"Shrinking '%s' worker's pool by '%s'\", workername, n)\n        response = self.capp.control.pool_shrink(\n            n=n, reply=True, destination=[workername])\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=f\"Shrinking '{workername}' worker's pool by {n}\"))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to shrink '{workername}' worker's pool: {reason}\")\n\n\nclass WorkerPoolAutoscale(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nAutoscale worker pool\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/pool/autoscale/celery@worker2?min=3&max=10 HTTP/1.1\n  Content-Length: 0\n  Content-Type: application/x-www-form-urlencoded; charset=utf-8\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 66\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Autoscaling 'celery@worker2' worker (min=3, max=10)\"\n  }\n\n:query min: minimum number of pool processes\n:query max: maximum number of pool processes\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: autoscaling is not enabled (see CELERYD_AUTOSCALER)\n:statuscode 404: unknown worker\n        \"\"\"\n\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        min = self.get_argument('min', type=int)\n        max = self.get_argument('max', type=int)\n\n        logger.info(\"Autoscaling '%s' worker by '%s'\",\n                    workername, (min, max))\n        response = self.capp.control.broadcast(\n            'autoscale', arguments={'min': min, 'max': max},\n            destination=[workername], reply=True)\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=f\"Autoscaling '{workername}' worker \"\n                                    \"(min={min}, max={max})\"))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to autoscale '{workername}' worker: {reason}\")\n\n\nclass WorkerQueueAddConsumer(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nStart consuming from a queue\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/queue/add-consumer/celery@worker2?queue=sample-queue\n  Content-Length: 0\n  Content-Type: application/x-www-form-urlencoded; charset=utf-8\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 40\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"add consumer sample-queue\"\n  }\n\n:query queue: the name of a new queue\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: failed to add consumer\n:statuscode 404: unknown worker\n        \"\"\"\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        queue = self.get_argument('queue')\n\n        logger.info(\"Adding consumer '%s' to worker '%s'\",\n                    queue, workername)\n        response = self.capp.control.broadcast(\n            'add_consumer', arguments={'queue': queue},\n            destination=[workername], reply=True)\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=response[0][workername]['ok']))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to add '{queue}' consumer to '{workername}' worker: {reason}\")\n\n\nclass WorkerQueueCancelConsumer(ControlHandler):\n    @web.authenticated\n    def post(self, workername):\n        \"\"\"\nStop consuming from a queue\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/worker/queue/cancel-consumer/celery@worker2?queue=sample-queue\n  Content-Length: 0\n  Content-Type: application/x-www-form-urlencoded; charset=utf-8\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 52\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"no longer consuming from sample-queue\"\n  }\n\n:query queue: the name of queue\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 403: failed to cancel consumer\n:statuscode 404: unknown worker\n        \"\"\"\n        if not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        queue = self.get_argument('queue')\n\n        logger.info(\"Canceling consumer '%s' from worker '%s'\",\n                    queue, workername)\n        response = self.capp.control.broadcast(\n            'cancel_consumer', arguments={'queue': queue},\n            destination=[workername], reply=True)\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=response[0][workername]['ok']))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(workername, response)\n            self.write(f\"Failed to cancel '{queue}' consumer from '{workername}' worker: {reason}\")\n\n\nclass TaskRevoke(ControlHandler):\n    @web.authenticated\n    def post(self, taskid):\n        \"\"\"\nRevoke a task\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/task/revoke/1480b55c-b8b2-462c-985e-24af3e9158f9?terminate=true\n  Content-Length: 0\n  Content-Type: application/x-www-form-urlencoded; charset=utf-8\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 61\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Revoked '1480b55c-b8b2-462c-985e-24af3e9158f9'\"\n  }\n\n:query terminate: terminate the task if it is running\n:query signal: name of signal to send to process if terminate (default: 'SIGTERM')\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n        \"\"\"\n        logger.info(\"Revoking task '%s'\", taskid)\n        terminate = self.get_argument('terminate', default=False, type=bool)\n        signal = self.get_argument('signal', default='SIGTERM', type=str)\n        self.capp.control.revoke(taskid, terminate=terminate, signal=signal)\n        self.write(dict(message=f\"Revoked '{taskid}'\"))\n\n\nclass TaskTimout(ControlHandler):\n    @web.authenticated\n    def post(self, taskname):\n        \"\"\"\nChange soft and hard time limits for a task\n\n**Example request**:\n\n.. sourcecode:: http\n\n    POST /api/task/timeout/tasks.sleep HTTP/1.1\n    Content-Length: 44\n    Content-Type: application/x-www-form-urlencoded; charset=utf-8\n    Host: localhost:5555\n\n    soft=30&hard=100&workername=celery%40worker1\n\n**Example response**:\n\n.. sourcecode:: http\n\n    HTTP/1.1 200 OK\n    Content-Length: 46\n    Content-Type: application/json; charset=UTF-8\n\n    {\n        \"message\": \"time limits set successfully\"\n    }\n\n:query workername: worker name\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task/worker\n        \"\"\"\n        workername = self.get_argument('workername')\n        hard = self.get_argument('hard', default=None, type=float)\n        soft = self.get_argument('soft', default=None, type=float)\n\n        if taskname not in self.capp.tasks:\n            raise web.HTTPError(404, f\"Unknown task '{taskname}'\")\n        if workername is not None and not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        logger.info(\"Setting timeouts for '%s' task (%s, %s)\",\n                    taskname, soft, hard)\n        destination = [workername] if workername is not None else None\n        response = self.capp.control.time_limit(\n            taskname, reply=True, hard=hard, soft=soft,\n            destination=destination)\n\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=response[0][workername]['ok']))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(taskname, response)\n            self.write(f\"Failed to set timeouts: '{reason}'\")\n\n\nclass TaskRateLimit(ControlHandler):\n    @web.authenticated\n    def post(self, taskname):\n        \"\"\"\nChange rate limit for a task\n\n**Example request**:\n\n.. sourcecode:: http\n\n    POST /api/task/rate-limit/tasks.sleep HTTP/1.1\n    Content-Length: 41\n    Content-Type: application/x-www-form-urlencoded; charset=utf-8\n    Host: localhost:5555\n\n    ratelimit=200&workername=celery%40worker1\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 61\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"new rate limit set successfully\"\n  }\n\n:query workername: worker name\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task/worker\n        \"\"\"\n        workername = self.get_argument('workername')\n        ratelimit = self.get_argument('ratelimit')\n\n        if taskname not in self.capp.tasks:\n            raise web.HTTPError(404, f\"Unknown task '{taskname}'\")\n        if workername is not None and not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        logger.info(\"Setting '%s' rate limit for '%s' task\",\n                    ratelimit, taskname)\n        destination = [workername] if workername is not None else None\n        response = self.capp.control.rate_limit(\n            taskname, ratelimit, reply=True, destination=destination)\n        if response and 'ok' in response[0][workername]:\n            self.write(dict(message=response[0][workername]['ok']))\n        else:\n            logger.error(response)\n            self.set_status(403)\n            reason = self.error_reason(taskname, response)\n            self.write(f\"Failed to set rate limit: '{reason}'\")\n"
  },
  {
    "path": "flower/api/tasks.py",
    "content": "import json\nimport logging\nfrom collections import OrderedDict\nfrom datetime import datetime\n\nfrom celery import states\nfrom celery.backends.base import DisabledBackend\nfrom celery.contrib.abortable import AbortableAsyncResult\nfrom celery.result import AsyncResult\nfrom tornado import web\nfrom tornado.escape import json_decode\nfrom tornado.ioloop import IOLoop\nfrom tornado.web import HTTPError\n\nfrom ..utils import tasks\nfrom ..utils.broker import Broker\nfrom . import BaseApiHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseTaskHandler(BaseApiHandler):\n    DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f'\n\n    def get_task_args(self):\n        try:\n            body = self.request.body\n            options = json_decode(body) if body else {}\n        except ValueError as e:\n            raise HTTPError(400, str(e)) from e\n\n        if not isinstance(options, dict):\n            raise HTTPError(400, 'invalid options')\n\n        args = options.pop('args', [])\n        kwargs = options.pop('kwargs', {})\n\n        if not isinstance(args, (list, tuple)):\n            raise HTTPError(400, 'args must be an array')\n\n        return args, kwargs, options\n\n    @staticmethod\n    def backend_configured(result):\n        return not isinstance(result.backend, DisabledBackend)\n\n    def write_error(self, status_code, **kwargs):\n        self.set_status(status_code)\n\n    def update_response_result(self, response, result):\n        if result.state == states.FAILURE:\n            response.update({'result': self.safe_result(result.result),\n                             'traceback': result.traceback})\n        else:\n            response.update({'result': self.safe_result(result.result)})\n\n    def normalize_options(self, options):\n        if 'eta' in options:\n            options['eta'] = datetime.strptime(options['eta'],\n                                               self.DATE_FORMAT)\n        if 'countdown' in options:\n            options['countdown'] = float(options['countdown'])\n        if 'expires' in options:\n            expires = options['expires']\n            try:\n                expires = float(expires)\n            except ValueError:\n                expires = datetime.strptime(expires, self.DATE_FORMAT)\n            options['expires'] = expires\n\n    def safe_result(self, result):\n        \"returns json encodable result\"\n        try:\n            json.dumps(result)\n        except TypeError:\n            return repr(result)\n        return result\n\n\nclass TaskApply(BaseTaskHandler):\n    @web.authenticated\n    async def post(self, taskname):\n        \"\"\"\nExecute a task by name and wait results\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/task/apply/tasks.add HTTP/1.1\n  Accept: application/json\n  Accept-Encoding: gzip, deflate, compress\n  Content-Length: 16\n  Content-Type: application/json; charset=utf-8\n  Host: localhost:5555\n\n  {\n      \"args\": [1, 2]\n  }\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 71\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"state\": \"SUCCESS\",\n      \"task-id\": \"c60be250-fe52-48df-befb-ac66174076e6\",\n      \"result\": 3\n  }\n\n:query args: a list of arguments\n:query kwargs: a dictionary of arguments\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task\n        \"\"\"\n        args, kwargs, options = self.get_task_args()\n        logger.debug(\"Invoking a task '%s' with '%s' and '%s'\",\n                     taskname, args, kwargs)\n\n        try:\n            task = self.capp.tasks[taskname]\n        except KeyError as exc:\n            raise HTTPError(404, f\"Unknown task '{taskname}'\") from exc\n\n        try:\n            self.normalize_options(options)\n        except ValueError as exc:\n            raise HTTPError(400, 'Invalid option') from exc\n\n        result = task.apply_async(args=args, kwargs=kwargs, **options)\n        response = {'task-id': result.task_id}\n\n        response = await IOLoop.current().run_in_executor(\n            None, self.wait_results, result, response)\n        self.write(response)\n\n    def wait_results(self, result, response):\n        # Wait until task finished and do not raise anything\n        result.get(propagate=False)\n        # Write results and finish async function\n        self.update_response_result(response, result)\n        if self.backend_configured(result):\n            response.update(state=result.state)\n        return response\n\n\nclass TaskAsyncApply(BaseTaskHandler):\n\n    @web.authenticated\n    def post(self, taskname):\n        \"\"\"\nExecute a task\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/task/async-apply/tasks.add HTTP/1.1\n  Accept: application/json\n  Accept-Encoding: gzip, deflate, compress\n  Content-Length: 16\n  Content-Type: application/json; charset=utf-8\n  Host: localhost:5555\n\n  {\n      \"args\": [1, 2]\n  }\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 71\n  Content-Type: application/json; charset=UTF-8\n  Date: Sun, 13 Apr 2014 15:55:00 GMT\n\n  {\n      \"state\": \"PENDING\",\n      \"task-id\": \"abc300c7-2922-4069-97b6-a635cc2ac47c\"\n  }\n\n:query args: a list of arguments\n:query kwargs: a dictionary of arguments\n:query options: a dictionary of `apply_async` keyword arguments\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task\n        \"\"\"\n        args, kwargs, options = self.get_task_args()\n        logger.debug(\"Invoking a task '%s' with '%s' and '%s'\",\n                     taskname, args, kwargs)\n\n        try:\n            task = self.capp.tasks[taskname]\n        except KeyError as exc:\n            raise HTTPError(404, f\"Unknown task '{taskname}'\") from exc\n\n        try:\n            self.normalize_options(options)\n        except ValueError as exc:\n            raise HTTPError(400, 'Invalid option') from exc\n\n        result = task.apply_async(args=args, kwargs=kwargs, **options)\n        response = {'task-id': result.task_id}\n        if self.backend_configured(result):\n            response.update(state=result.state)\n        self.write(response)\n\n\nclass TaskSend(BaseTaskHandler):\n    @web.authenticated\n    def post(self, taskname):\n        \"\"\"\nExecute a task by name (doesn't require task sources)\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/task/send-task/tasks.add HTTP/1.1\n  Accept: application/json\n  Accept-Encoding: gzip, deflate, compress\n  Content-Length: 16\n  Content-Type: application/json; charset=utf-8\n  Host: localhost:5555\n\n  {\n      \"args\": [1, 2]\n  }\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 71\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"state\": \"SUCCESS\",\n      \"task-id\": \"c60be250-fe52-48df-befb-ac66174076e6\"\n  }\n\n:query args: a list of arguments\n:query kwargs: a dictionary of arguments\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task\n        \"\"\"\n        args, kwargs, options = self.get_task_args()\n        logger.debug(\"Invoking task '%s' with '%s' and '%s'\",\n                     taskname, args, kwargs)\n        result = self.capp.send_task(\n            taskname, args=args, kwargs=kwargs, **options)\n        response = {'task-id': result.task_id}\n        if self.backend_configured(result):\n            response.update(state=result.state)\n        self.write(response)\n\n\nclass TaskResult(BaseTaskHandler):\n    @web.authenticated\n    def get(self, taskid):\n        \"\"\"\nGet a task result\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/task/result/c60be250-fe52-48df-befb-ac66174076e6 HTTP/1.1\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 84\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"result\": 3,\n      \"state\": \"SUCCESS\",\n      \"task-id\": \"c60be250-fe52-48df-befb-ac66174076e6\"\n  }\n\n:query timeout: how long to wait, in seconds, before the operation times out\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 503: result backend is not configured\n        \"\"\"\n        timeout = self.get_argument('timeout', None)\n        timeout = float(timeout) if timeout is not None else None\n\n        result = AsyncResult(taskid)\n        if not self.backend_configured(result):\n            raise HTTPError(503)\n        response = {'task-id': taskid, 'state': result.state}\n\n        if timeout:\n            result.get(timeout=timeout, propagate=False)\n            self.update_response_result(response, result)\n        elif result.ready():\n            self.update_response_result(response, result)\n        self.write(response)\n\n\nclass TaskAbort(BaseTaskHandler):\n    @web.authenticated\n    def post(self, taskid):\n        \"\"\"\nAbort a running task\n\n**Example request**:\n\n.. sourcecode:: http\n\n  POST /api/task/abort/c60be250-fe52-48df-befb-ac66174076e6 HTTP/1.1\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 61\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"message\": \"Aborted '1480b55c-b8b2-462c-985e-24af3e9158f9'\"\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 503: result backend is not configured\n        \"\"\"\n        logger.info(\"Aborting task '%s'\", taskid)\n\n        result = AbortableAsyncResult(taskid)\n        if not self.backend_configured(result):\n            raise HTTPError(503)\n\n        result.abort()\n\n        self.write(dict(message=f\"Aborted '{taskid}'\"))\n\n\nclass GetQueueLengths(BaseTaskHandler):\n    @web.authenticated\n    async def get(self):\n        \"\"\"\nReturn length of all active queues\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/queues/length\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 94\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"active_queues\": [\n          {\"name\": \"celery\", \"messages\": 0},\n          {\"name\": \"video-queue\", \"messages\": 5}\n      ]\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 503: result backend is not configured\n        \"\"\"\n        app = self.application\n\n        http_api = None\n        if app.transport == 'amqp' and app.options.broker_api:\n            http_api = app.options.broker_api\n\n        broker = Broker(app.capp.connection().as_uri(include_password=True),\n                        http_api=http_api, broker_options=self.capp.conf.broker_transport_options,\n                        broker_use_ssl=self.capp.conf.broker_use_ssl)\n\n        queues = await broker.queues(self.get_active_queue_names())\n        self.write({'active_queues': queues})\n\n\nclass ListTasks(BaseTaskHandler):\n    @web.authenticated\n    def get(self):\n        \"\"\"\nList tasks\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/tasks HTTP/1.1\n  Host: localhost:5555\n  User-Agent: HTTPie/0.8.0\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 1109\n  Content-Type: application/json; charset=UTF-8\n  Etag: \"b2478118015c8b825f7b88ce6b660e5449746c37\"\n  Server: TornadoServer/3.1.1\n\n  {\n      \"e42ceb2d-8730-47b5-8b4d-8e0d2a1ef7c9\": {\n          \"args\": \"[3, 4]\",\n          \"client\": null,\n          \"clock\": 1079,\n          \"eta\": null,\n          \"exception\": null,\n          \"exchange\": null,\n          \"expires\": null,\n          \"failed\": null,\n          \"kwargs\": \"{}\",\n          \"name\": \"tasks.add\",\n          \"received\": 1398505411.107885,\n          \"result\": \"'7'\",\n          \"retried\": null,\n          \"retries\": 0,\n          \"revoked\": null,\n          \"routing_key\": null,\n          \"runtime\": 0.01610181899741292,\n          \"sent\": null,\n          \"started\": 1398505411.108985,\n          \"state\": \"SUCCESS\",\n          \"succeeded\": 1398505411.124802,\n          \"timestamp\": 1398505411.124802,\n          \"traceback\": null,\n          \"uuid\": \"e42ceb2d-8730-47b5-8b4d-8e0d2a1ef7c9\",\n          \"worker\": \"celery@worker1\"\n      },\n      \"f67ea225-ae9e-42a8-90b0-5de0b24507e0\": {\n          \"args\": \"[1, 2]\",\n          \"client\": null,\n          \"clock\": 1042,\n          \"eta\": null,\n          \"exception\": null,\n          \"exchange\": null,\n          \"expires\": null,\n          \"failed\": null,\n          \"kwargs\": \"{}\",\n          \"name\": \"tasks.add\",\n          \"received\": 1398505395.327208,\n          \"result\": \"'3'\",\n          \"retried\": null,\n          \"retries\": 0,\n          \"revoked\": null,\n          \"routing_key\": null,\n          \"runtime\": 0.012884548006695695,\n          \"sent\": null,\n          \"started\": 1398505395.3289,\n          \"state\": \"SUCCESS\",\n          \"succeeded\": 1398505395.341089,\n          \"timestamp\": 1398505395.341089,\n          \"traceback\": null,\n          \"uuid\": \"f67ea225-ae9e-42a8-90b0-5de0b24507e0\",\n          \"worker\": \"celery@worker1\"\n      }\n  }\n\n:query limit: maximum number of tasks\n:query offset: skip first n tasks\n:query sort_by: sort tasks by attribute (name, state, received, started)\n:query workername: filter task by workername\n:query taskname: filter tasks by taskname\n:query state: filter tasks by state\n:query received_start: filter tasks by received date (must be greater than) format %Y-%m-%d %H:%M\n:query received_end: filter tasks by received date (must be less than) format %Y-%m-%d %H:%M\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n        \"\"\"\n        app = self.application\n        limit = self.get_argument('limit', None)\n        offset = self.get_argument('offset', default=0, type=int)\n        worker = self.get_argument('workername', None)\n        type = self.get_argument('taskname', None)\n        state = self.get_argument('state', None)\n        received_start = self.get_argument('received_start', None)\n        received_end = self.get_argument('received_end', None)\n        sort_by = self.get_argument('sort_by', None)\n        search = self.get_argument('search', None)\n\n        limit = limit and int(limit)\n        offset = max(offset, 0)\n        worker = worker if worker != 'All' else None\n        type = type if type != 'All' else None\n        state = state if state != 'All' else None\n\n        result = []\n        for task_id, task in tasks.iter_tasks(\n                app.events, limit=limit, offset=offset, sort_by=sort_by, type=type,\n                worker=worker, state=state,\n                received_start=received_start,\n                received_end=received_end,\n                search=search\n        ):\n            task = tasks.as_dict(task)\n            worker = task.pop('worker', None)\n            if worker is not None:\n                task['worker'] = worker.hostname\n            result.append((task_id, task))\n        self.write(OrderedDict(result))\n\n\nclass ListTaskTypes(BaseTaskHandler):\n    @web.authenticated\n    def get(self):\n        \"\"\"\nList (seen) task types\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/task/types HTTP/1.1\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 44\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"task-types\": [\n          \"tasks.add\",\n          \"tasks.sleep\"\n      ]\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n        \"\"\"\n        seen_task_types = self.application.events.state.task_types()\n\n        response = {}\n        response['task-types'] = seen_task_types\n        self.write(response)\n\n\nclass TaskInfo(BaseTaskHandler):\n    @web.authenticated\n    def get(self, taskid):\n        \"\"\"\nGet a task info\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/task/info/91396550-c228-4111-9da4-9d88cfd5ddc6 HTTP/1.1\n  Accept: */*\n  Accept-Encoding: gzip, deflate, compress\n  Host: localhost:5555\n\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 575\n  Content-Type: application/json; charset=UTF-8\n\n  {\n      \"args\": \"[2, 2]\",\n      \"client\": null,\n      \"clock\": 25,\n      \"eta\": null,\n      \"exception\": null,\n      \"exchange\": null,\n      \"expires\": null,\n      \"failed\": null,\n      \"kwargs\": \"{}\",\n      \"name\": \"tasks.add\",\n      \"received\": 1400806241.970742,\n      \"result\": \"'4'\",\n      \"retried\": null,\n      \"retries\": null,\n      \"revoked\": null,\n      \"routing_key\": null,\n      \"runtime\": 2.0037889280356467,\n      \"sent\": null,\n      \"started\": 1400806241.972624,\n      \"state\": \"SUCCESS\",\n      \"succeeded\": 1400806243.975336,\n      \"task-id\": \"91396550-c228-4111-9da4-9d88cfd5ddc6\",\n      \"timestamp\": 1400806243.975336,\n      \"traceback\": null,\n      \"worker\": \"celery@worker1\"\n  }\n\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n:statuscode 404: unknown task\n        \"\"\"\n\n        task = tasks.get_task_by_id(self.application.events, taskid)\n        if not task:\n            raise HTTPError(404, f\"Unknown task '{taskid}'\")\n\n        response = task.as_dict()\n        if task.worker is not None:\n            response['worker'] = task.worker.hostname\n\n        self.write(response)\n"
  },
  {
    "path": "flower/api/workers.py",
    "content": "import asyncio\nimport logging\n\nfrom tornado import web\n\nfrom .control import ControlHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass ListWorkers(ControlHandler):\n    @web.authenticated\n    async def get(self):\n        \"\"\"\nList workers\n\n**Example request**:\n\n.. sourcecode:: http\n\n  GET /api/workers HTTP/1.1\n  Host: localhost:5555\n\n**Example response**:\n\n.. sourcecode:: http\n\n  HTTP/1.1 200 OK\n  Content-Length: 1526\n  Content-Type: application/json; charset=UTF-8\n  Date: Tue, 28 Jul 2015 01:32:38 GMT\n  Etag: \"fcdd75d85a82b4052275e28871d199aac1ece21c\"\n  Server: TornadoServer/4.0.2\n\n  {\n      \"celery@worker1\": {\n          \"active_queues\": [\n              {\n                  \"alias\": null,\n                  \"auto_delete\": false,\n                  \"binding_arguments\": null,\n                  \"bindings\": [],\n                  \"durable\": true,\n                  \"exchange\": {\n                      \"arguments\": null,\n                      \"auto_delete\": false,\n                      \"delivery_mode\": 2,\n                      \"durable\": true,\n                      \"name\": \"celery\",\n                      \"passive\": false,\n                      \"type\": \"direct\"\n                  },\n                  \"exclusive\": false,\n                  \"name\": \"celery\",\n                  \"no_ack\": false,\n                  \"queue_arguments\": null,\n                  \"routing_key\": \"celery\"\n              }\n          ],\n          \"conf\": {\n              \"CELERYBEAT_SCHEDULE\": {},\n              \"CELERY_INCLUDE\": [\n                  \"celery.app.builtins\",\n                  \"__main__\"\n              ],\n              \"CELERY_SEND_TASK_SENT_EVENT\": true,\n              \"CELERY_TIMEZONE\": \"UTC\"\n          },\n          \"registered\": [\n              \"tasks.add\",\n              \"tasks.echo\",\n              \"tasks.error\",\n              \"tasks.retry\",\n              \"tasks.sleep\"\n          ],\n          \"stats\": {\n              \"broker\": {\n                  \"alternates\": [],\n                  \"connect_timeout\": 4,\n                  \"heartbeat\": null,\n                  \"hostname\": \"127.0.0.1\",\n                  \"insist\": false,\n                  \"login_method\": \"AMQPLAIN\",\n                  \"port\": 5672,\n                  \"ssl\": false,\n                  \"transport\": \"amqp\",\n                  \"transport_options\": {},\n                  \"uri_prefix\": null,\n                  \"userid\": \"guest\",\n                  \"virtual_host\": \"/\"\n              },\n              \"clock\": \"918\",\n              \"pid\": 90494,\n              \"pool\": {\n                  \"max-concurrency\": 4,\n                  \"max-tasks-per-child\": \"N/A\",\n                  \"processes\": [\n                      90499,\n                      90500,\n                      90501,\n                      90502\n                  ],\n                  \"put-guarded-by-semaphore\": false,\n                  \"timeouts\": [\n                      0,\n                      0\n                  ],\n                  \"writes\": {\n                      \"all\": \"100.00%\",\n                      \"avg\": \"100.00%\",\n                      \"inqueues\": {\n                          \"active\": 0,\n                          \"total\": 4\n                      },\n                      \"raw\": \"1\",\n                      \"total\": 1\n                  }\n              },\n              \"prefetch_count\": 16,\n              \"rusage\": {\n                  \"idrss\": 0,\n                  \"inblock\": 211,\n                  \"isrss\": 0,\n                  \"ixrss\": 0,\n                  \"majflt\": 6,\n                  \"maxrss\": 26996736,\n                  \"minflt\": 11450,\n                  \"msgrcv\": 4968,\n                  \"msgsnd\": 1227,\n                  \"nivcsw\": 1367,\n                  \"nsignals\": 0,\n                  \"nswap\": 0,\n                  \"nvcsw\": 1855,\n                  \"oublock\": 93,\n                  \"stime\": 0.414564,\n                  \"utime\": 0.975726\n              },\n              \"total\": {\n                  \"tasks.add\": 1\n              }\n          },\n          \"timestamp\": 1438049312.073402\n      }\n  }\n\n:query refresh: run inspect to get updated list of workers\n:query workername: get info for workername\n:query status: only get worker status info\n:reqheader Authorization: optional OAuth token to authenticate\n:statuscode 200: no error\n:statuscode 401: unauthorized request\n        \"\"\"\n        refresh = self.get_argument('refresh', default=False, type=bool)\n        status = self.get_argument('status', default=False, type=bool)\n        workername = self.get_argument('workername', default=None)\n\n        if refresh:\n            try:\n                await asyncio.wait(self.application.update_workers(workername=workername))\n            except Exception as e:\n                msg = f\"Failed to update workers: {e}\"\n                logger.error(msg)\n                raise web.HTTPError(503, msg)\n\n        if status:\n            info = {}\n            for name, worker in self.application.events.state.workers.items():\n                info[name] = worker.alive\n            self.write(info)\n            return\n\n        if self.application.workers and not refresh and\\\n                workername in self.application.workers:\n            self.write({workername: self.application.workers[workername]})\n            return\n\n        if workername and not self.is_worker(workername):\n            raise web.HTTPError(404, f\"Unknown worker '{workername}'\")\n\n        if workername:\n            self.write({workername: self.application.workers[workername]})\n        else:\n            self.write(self.application.workers)\n"
  },
  {
    "path": "flower/app.py",
    "content": "import sys\nimport logging\n\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport celery\nimport tornado.web\n\nfrom tornado import ioloop\nfrom tornado.httpserver import HTTPServer\nfrom tornado.web import url\n\nfrom .urls import handlers as default_handlers\nfrom .events import Events\nfrom .inspector import Inspector\nfrom .options import default_options\n\n\nlogger = logging.getLogger(__name__)\n\n\nif sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):\n    import asyncio\n    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())\n\n# pylint: disable=consider-using-f-string\ndef rewrite_handler(handler, url_prefix):\n    if isinstance(handler, url):\n        return url(\"/{}{}\".format(url_prefix.strip(\"/\"), handler.regex.pattern),\n                   handler.handler_class, handler.kwargs, handler.name)\n    return (\"/{}{}\".format(url_prefix.strip(\"/\"), handler[0]), handler[1])\n\n\nclass Flower(tornado.web.Application):\n    pool_executor_cls = ThreadPoolExecutor\n    max_workers = None\n\n    def __init__(self, options=None, capp=None, events=None,\n                 io_loop=None, **kwargs):\n        handlers = default_handlers\n        if options is not None and options.url_prefix:\n            handlers = [rewrite_handler(h, options.url_prefix) for h in handlers]\n        kwargs.update(handlers=handlers)\n        super().__init__(**kwargs)\n        self.options = options or default_options\n        self.io_loop = io_loop or ioloop.IOLoop.instance()\n        self.ssl_options = kwargs.get('ssl_options', None)\n\n        self.capp = capp or celery.Celery()\n        self.capp.loader.import_default_modules()\n\n        self.executor = self.pool_executor_cls(max_workers=self.max_workers)\n        self.io_loop.set_default_executor(self.executor)\n\n        self.inspector = Inspector(self.io_loop, self.capp, self.options.inspect_timeout / 1000.0)\n\n        self.events = events or Events(\n            self.capp,\n            db=self.options.db,\n            persistent=self.options.persistent,\n            state_save_interval=self.options.state_save_interval,\n            enable_events=self.options.enable_events,\n            io_loop=self.io_loop,\n            max_workers_in_memory=self.options.max_workers,\n            max_tasks_in_memory=self.options.max_tasks)\n        self.started = False\n\n    def start(self):\n        self.events.start()\n\n        if not self.options.unix_socket:\n            self.listen(self.options.port, address=self.options.address,\n                        ssl_options=self.ssl_options,\n                        xheaders=self.options.xheaders)\n        else:\n            from tornado.netutil import bind_unix_socket\n            server = HTTPServer(self)\n            socket = bind_unix_socket(self.options.unix_socket, mode=0o777)\n            server.add_socket(socket)\n\n        self.started = True\n        self.update_workers()\n        self.io_loop.start()\n\n    def stop(self):\n        if self.started:\n            self.events.stop()\n            logging.debug(\"Stopping executors...\")\n            self.executor.shutdown(wait=False)\n            logging.debug(\"Stopping event loop...\")\n            self.io_loop.stop()\n            self.started = False\n\n    @property\n    def transport(self):\n        return getattr(self.capp.connection().transport, 'driver_type', None)\n\n    @property\n    def workers(self):\n        return self.inspector.workers\n\n    def update_workers(self, workername=None):\n        return self.inspector.inspect(workername)\n"
  },
  {
    "path": "flower/command.py",
    "content": "import os\nimport sys\nimport atexit\nimport signal\nimport logging\n\nfrom pprint import pformat\n\nfrom logging import NullHandler\n\nimport click\nfrom tornado.options import options\nfrom tornado.options import parse_command_line, parse_config_file\nfrom tornado.log import enable_pretty_logging\nfrom celery.bin.base import CeleryCommand\n\nfrom .app import Flower\nfrom .urls import settings\nfrom .utils import abs_path, prepend_url, strtobool\nfrom .options import DEFAULT_CONFIG_FILE, default_options\nfrom .views.auth import validate_auth_option\n\nlogger = logging.getLogger(__name__)\nENV_VAR_PREFIX = 'FLOWER_'\n\n\ndef sigterm_handler(signum, _):\n    logger.info('%s detected, shutting down', signum)\n    sys.exit(0)\n\n\n@click.command(cls=CeleryCommand,\n               context_settings={\n                   'ignore_unknown_options': True\n               })\n@click.argument(\"tornado_argv\", nargs=-1, type=click.UNPROCESSED)\n@click.pass_context\ndef flower(ctx, tornado_argv):\n    \"\"\"Web based tool for monitoring and administrating Celery clusters.\"\"\"\n    warn_about_celery_args_used_in_flower_command(ctx, tornado_argv)\n    apply_env_options()\n    apply_options(sys.argv[0], tornado_argv)\n\n    extract_settings()\n    setup_logging()\n\n    app = ctx.obj.app\n    flower_app = Flower(capp=app, options=options, **settings)\n\n    atexit.register(flower_app.stop)\n    signal.signal(signal.SIGTERM, sigterm_handler)\n\n    if not ctx.obj.quiet:\n        print_banner(app, 'ssl_options' in settings)\n\n    try:\n        flower_app.start()\n    except (KeyboardInterrupt, SystemExit):\n        pass\n\n\ndef apply_env_options():\n    \"apply options passed through environment variables\"\n    env_options = filter(is_flower_envvar, os.environ)\n    for env_var_name in env_options:\n        name = env_var_name.replace(ENV_VAR_PREFIX, '', 1).lower()\n        value = os.environ[env_var_name]\n        try:\n            option = options._options[name]  # pylint: disable=protected-access\n        except KeyError:\n            option = options._options[name.replace('_', '-')]  # pylint: disable=protected-access\n        if option.multiple:\n            value = [option.type(i) for i in value.split(',')]\n        else:\n            if option.type is bool:\n                value = bool(strtobool(value))\n            else:\n                value = option.type(value)\n        setattr(options, name, value)\n\n\ndef apply_options(prog_name, argv):\n    \"apply options passed through the configuration file\"\n    argv = list(filter(is_flower_option, argv))\n    # parse the command line to get --conf option\n    parse_command_line([prog_name] + argv)\n    try:\n        parse_config_file(os.path.abspath(options.conf), final=False)\n        parse_command_line([prog_name] + argv)\n    except IOError:\n        if os.path.basename(options.conf) != DEFAULT_CONFIG_FILE:\n            raise\n\n\ndef warn_about_celery_args_used_in_flower_command(ctx, flower_args):\n    celery_options = [option for param in ctx.parent.command.params for option in param.opts]\n\n    incorrectly_used_args = []\n    for arg in flower_args:\n        arg_name, _, _ = arg.partition(\"=\")\n        if arg_name in celery_options:\n            incorrectly_used_args.append(arg_name)\n\n    if incorrectly_used_args:\n        logger.warning(\n            'You have incorrectly specified the following celery arguments after flower command:'\n            ' %s. '\n            'Please specify them after celery command instead following this template: '\n            'celery [celery args] flower [flower args].', incorrectly_used_args\n        )\n\n\ndef setup_logging():\n    if options.debug and options.logging == 'info':\n        options.logging = 'debug'\n        enable_pretty_logging()\n    else:\n        logging.getLogger(\"tornado.access\").addHandler(NullHandler())\n        logging.getLogger(\"tornado.access\").propagate = False\n\n\ndef extract_settings():\n    settings['debug'] = options.debug\n\n    if options.cookie_secret:\n        settings['cookie_secret'] = options.cookie_secret\n\n    if options.url_prefix:\n        for name in ['login_url', 'static_url_prefix']:\n            settings[name] = prepend_url(settings[name], options.url_prefix)\n\n    if options.auth:\n        settings['oauth'] = {\n            'key': options.oauth2_key or os.environ.get('FLOWER_OAUTH2_KEY'),\n            'secret': options.oauth2_secret or os.environ.get('FLOWER_OAUTH2_SECRET'),\n            'redirect_uri': options.oauth2_redirect_uri or os.environ.get('FLOWER_OAUTH2_REDIRECT_URI'),\n        }\n\n    if options.certfile and options.keyfile:\n        settings['ssl_options'] = dict(certfile=abs_path(options.certfile),\n                                       keyfile=abs_path(options.keyfile))\n        if options.ca_certs:\n            settings['ssl_options']['ca_certs'] = abs_path(options.ca_certs)\n\n    if options.auth and not validate_auth_option(options.auth):\n        logger.error(\"Invalid '--auth' option: %s\", options.auth)\n        sys.exit(1)\n\n\ndef is_flower_option(arg):\n    name, _, _ = arg.lstrip('-').partition(\"=\")\n    name = name.replace('-', '_')\n    return hasattr(options, name)\n\n\ndef is_flower_envvar(name):\n    return name.startswith(ENV_VAR_PREFIX) and \\\n        name[len(ENV_VAR_PREFIX):].lower() in default_options\n\n\ndef print_banner(app, ssl):\n    if not options.unix_socket:\n        if options.url_prefix:\n            prefix_str = f'/{options.url_prefix}/'\n        else:\n            prefix_str = ''\n\n        logger.info(\n            \"Visit me at http%s://%s:%s%s\", 's' if ssl else '',\n            options.address or '0.0.0.0', options.port,\n            prefix_str\n        )\n    else:\n        logger.info(\"Visit me via unix socket file: %s\", options.unix_socket)\n\n    logger.info('Broker: %s', app.connection().as_uri())\n    logger.info(\n        'Registered tasks: \\n%s',\n        pformat(sorted(app.tasks.keys()))\n    )\n    logger.debug('Settings: %s', pformat(settings))\n"
  },
  {
    "path": "flower/events.py",
    "content": "import collections\nimport logging\nimport shelve\nimport threading\nimport time\nfrom collections import Counter\nfrom functools import partial\n\nfrom celery.events import EventReceiver\nfrom celery.events.state import State\nfrom prometheus_client import Counter as PrometheusCounter\nfrom prometheus_client import Gauge, Histogram\nfrom tornado.ioloop import PeriodicCallback\nfrom tornado.options import options\n\nlogger = logging.getLogger(__name__)\n\nPROMETHEUS_METRICS = None\n\n\ndef get_prometheus_metrics():\n    global PROMETHEUS_METRICS  # pylint: disable=global-statement\n    if PROMETHEUS_METRICS is None:\n        PROMETHEUS_METRICS = PrometheusMetrics()\n\n    return PROMETHEUS_METRICS\n\n\nclass PrometheusMetrics:\n    def __init__(self):\n        self.events = PrometheusCounter('flower_events_total', \"Number of events\", ['worker', 'type', 'task'])\n\n        self.runtime = Histogram(\n            'flower_task_runtime_seconds',\n            \"Task runtime\",\n            ['worker', 'task'],\n            buckets=options.task_runtime_metric_buckets\n        )\n        self.prefetch_time = Gauge(\n            'flower_task_prefetch_time_seconds',\n            \"The time the task spent waiting at the celery worker to be executed.\",\n            ['worker', 'task']\n        )\n        self.number_of_prefetched_tasks = Gauge(\n            'flower_worker_prefetched_tasks',\n            'Number of tasks of given type prefetched at a worker',\n            ['worker', 'task']\n        )\n        self.worker_online = Gauge('flower_worker_online', \"Worker online status\", ['worker'])\n        self.worker_number_of_currently_executing_tasks = Gauge(\n            'flower_worker_number_of_currently_executing_tasks',\n            \"Number of tasks currently executing at a worker\",\n            ['worker']\n        )\n\n\nclass EventsState(State):\n    # EventsState object is created and accessed only from ioloop thread\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.counter = collections.defaultdict(Counter)\n        self.metrics = get_prometheus_metrics()\n\n    def event(self, event):\n        # Save the event\n        super().event(event)\n\n        worker_name = event['hostname']\n        event_type = event['type']\n\n        self.counter[worker_name][event_type] += 1\n\n        if event_type.startswith('task-'):\n            task_id = event['uuid']\n            task = self.tasks.get(task_id)\n            task_name = event.get('name', '')\n            if not task_name and task_id in self.tasks:\n                task_name = task.name or ''\n            self.metrics.events.labels(worker_name, event_type, task_name).inc()\n\n            runtime = event.get('runtime', 0)\n            if runtime:\n                self.metrics.runtime.labels(worker_name, task_name).observe(runtime)\n\n            task_started = task.started\n            task_received = task.received\n\n            if event_type == 'task-received' and not task.eta and task_received:\n                self.metrics.number_of_prefetched_tasks.labels(worker_name, task_name).inc()\n\n            if event_type == 'task-started' and not task.eta and task_started and task_received:\n                self.metrics.prefetch_time.labels(worker_name, task_name).set(task_started - task_received)\n                self.metrics.number_of_prefetched_tasks.labels(worker_name, task_name).dec()\n\n            if event_type in ['task-succeeded', 'task-failed'] and not task.eta and task_started and task_received:\n                self.metrics.prefetch_time.labels(worker_name, task_name).set(0)\n\n        if event_type == 'worker-online':\n            self.metrics.worker_online.labels(worker_name).set(1)\n\n        if event_type == 'worker-heartbeat':\n            self.metrics.worker_online.labels(worker_name).set(1)\n\n            num_executing_tasks = event.get('active')\n            if num_executing_tasks is not None:\n                self.metrics.worker_number_of_currently_executing_tasks.labels(worker_name).set(num_executing_tasks)\n\n        if event_type == 'worker-offline':\n            self.metrics.worker_online.labels(worker_name).set(0)\n\n\nclass Events(threading.Thread):\n    events_enable_interval = 5000\n\n    # pylint: disable=too-many-arguments\n    def __init__(self, capp, io_loop, db=None, persistent=False,\n                 enable_events=True, state_save_interval=0,\n                 **kwargs):\n        threading.Thread.__init__(self)\n        self.daemon = True\n\n        self.io_loop = io_loop\n        self.capp = capp\n\n        self.db = db\n        self.persistent = persistent\n        self.enable_events = enable_events\n        self.state = None\n        self.state_save_timer = None\n\n        if self.persistent:\n            logger.debug(\"Loading state from '%s'...\", self.db)\n            state = shelve.open(self.db)\n            if state:\n                self.state = state['events']\n            state.close()\n\n            if state_save_interval:\n                self.state_save_timer = PeriodicCallback(self.save_state,\n                                                         state_save_interval)\n\n        if not self.state:\n            self.state = EventsState(**kwargs)\n\n        self.timer = PeriodicCallback(self.on_enable_events,\n                                      self.events_enable_interval)\n\n    def start(self):\n        threading.Thread.start(self)\n        if self.enable_events:\n            logger.debug(\"Starting enable events timer...\")\n            self.timer.start()\n\n        if self.state_save_timer:\n            logger.debug(\"Starting state save timer...\")\n            self.state_save_timer.start()\n\n    def stop(self):\n        if self.enable_events:\n            logger.debug(\"Stopping enable events timer...\")\n            self.timer.stop()\n\n        if self.state_save_timer:\n            logger.debug(\"Stopping state save timer...\")\n            self.state_save_timer.stop()\n\n        if self.persistent:\n            self.save_state()\n\n    def run(self):\n        try_interval = 1\n        while True:\n            try:\n                try_interval *= 2\n\n                with self.capp.connection() as conn:\n                    recv = EventReceiver(conn,\n                                         handlers={\"*\": self.on_event},\n                                         app=self.capp)\n                    try_interval = 1\n                    logger.debug(\"Capturing events...\")\n                    recv.capture(limit=None, timeout=None, wakeup=True)\n            except (KeyboardInterrupt, SystemExit):\n                try:\n                    import _thread as thread\n                except ImportError:\n                    import thread\n                thread.interrupt_main()\n            except Exception as e:\n                logger.error(\"Failed to capture events: '%s', \"\n                             \"trying again in %s seconds.\",\n                             e, try_interval)\n                logger.debug(e, exc_info=True)\n                time.sleep(try_interval)\n\n    def save_state(self):\n        logger.debug(\"Saving state to '%s'...\", self.db)\n        state = shelve.open(self.db, flag='n')\n        state['events'] = self.state\n        state.close()\n\n    def on_enable_events(self):\n        # Periodically enable events for workers\n        # launched after flower\n        self.io_loop.run_in_executor(None, self.capp.control.enable_events)\n\n    def on_event(self, event):\n        # Call EventsState.event in ioloop thread to avoid synchronization\n        self.io_loop.add_callback(partial(self.state.event, event))\n"
  },
  {
    "path": "flower/inspector.py",
    "content": "import collections\nimport logging\nimport time\nfrom functools import partial\n\nlogger = logging.getLogger(__name__)\n\n\nclass Inspector:\n    methods = ('stats', 'active_queues', 'registered', 'scheduled',\n               'active', 'reserved', 'revoked', 'conf')\n\n    def __init__(self, io_loop, capp, timeout):\n        self.io_loop = io_loop\n        self.capp = capp\n        self.timeout = timeout\n        self.workers = collections.defaultdict(dict)\n\n    def inspect(self, workername=None):\n        feutures = []\n        for method in self.methods:\n            feutures.append(self.io_loop.run_in_executor(None, partial(self._inspect, method, workername)))\n        return feutures\n\n    def _on_update(self, workername, method, response):\n        info = self.workers[workername]\n        info[method] = response\n        info['timestamp'] = time.time()\n\n    def _inspect(self, method, workername):\n        destination = [workername] if workername else None\n        inspect = self.capp.control.inspect(timeout=self.timeout, destination=destination)\n\n        logger.debug('Sending %s inspect command', method)\n        start = time.time()\n        result = (\n            getattr(inspect, method)()\n            if method != 'active'\n            else getattr(inspect, method)(safe=True)\n        )\n        logger.debug(\"Inspect command %s took %.2fs to complete\", method, time.time() - start)\n\n        if result is None or 'error' in result:\n            logger.warning(\"Inspect method %s failed\", method)\n            return\n        for worker, response in result.items():\n            if response is not None:\n                self.io_loop.add_callback(partial(self._on_update, worker, method, response))\n"
  },
  {
    "path": "flower/options.py",
    "content": "import types\nfrom secrets import token_urlsafe\n\nfrom prometheus_client import Histogram\nfrom tornado.options import define, options\n\nDEFAULT_CONFIG_FILE = 'flowerconfig.py'\n\n\ndefine(\"port\", default=5555,\n       help=\"run on the given port\", type=int)\ndefine(\"address\", default='',\n       help=\"run on the given address\", type=str)\ndefine(\"unix_socket\", default='',\n       help=\"path to unix socket to bind\", type=str)\ndefine(\"debug\", default=False,\n       help=\"run in debug mode\", type=bool)\ndefine(\"inspect_timeout\", default=1000.0, type=float,\n       help=\"inspect timeout (in milliseconds)\")\ndefine(\"auth\", default='', type=str,\n       help=\"regexp of emails to grant access\")\ndefine(\"basic_auth\", type=str, default=None, multiple=True,\n       help=\"enable http basic authentication\")\ndefine(\"oauth2_key\", type=str, default=None,\n       help=\"OAuth2 key (requires --auth)\")\ndefine(\"oauth2_secret\", type=str, default=None,\n       help=\"OAuth2 secret (requires --auth)\")\ndefine(\"oauth2_redirect_uri\", type=str, default=None,\n       help=\"OAuth2 redirect uri (requires --auth)\")\ndefine(\"max_workers\", type=int, default=5000,\n       help=\"maximum number of workers to keep in memory\")\ndefine(\"max_tasks\", type=int, default=100000,\n       help=\"maximum number of tasks to keep in memory\")\ndefine(\"db\", type=str, default='flower',\n       help=\"flower database file\")\ndefine(\"persistent\", type=bool, default=False,\n       help=\"enable persistent mode\")\ndefine(\"state_save_interval\", type=int, default=0,\n       help=\"state save interval (in milliseconds)\")\ndefine(\"broker_api\", type=str, default=None,\n       help=\"inspect broker e.g. http://guest:guest@localhost:15672/api/\")\ndefine(\"ca_certs\", type=str, default=None,\n       help=\"SSL certificate authority (CA) file\")\ndefine(\"certfile\", type=str, default=None,\n       help=\"SSL certificate file\")\ndefine(\"keyfile\", type=str, default=None,\n       help=\"SSL key file\")\ndefine(\"xheaders\", type=bool, default=False,\n       help=\"enable support for the 'X-Real-Ip' and 'X-Scheme' headers.\")\ndefine(\"auto_refresh\", default=True,\n       help=\"refresh workerss\", type=bool)\ndefine(\"purge_offline_workers\", default=None, type=int,\n       help=\"time (in seconds) after which offline workers are purged from workers\")\ndefine(\"cookie_secret\", type=str, default=token_urlsafe(64),\n       help=\"secure cookie secret\")\ndefine(\"conf\", default=DEFAULT_CONFIG_FILE,\n       help=\"configuration file\")\ndefine(\"enable_events\", type=bool, default=True,\n       help=\"periodically enable Celery events\")\ndefine(\"format_task\", type=types.FunctionType, default=None,\n       help=\"use custom task formatter\")\ndefine(\"natural_time\", type=bool, default=False,\n       help=\"show time in relative format\")\ndefine(\"tasks_columns\", type=str,\n       default=\"name,uuid,state,args,kwargs,result,received,started,runtime,worker\",\n       help=\"slugs of columns on /tasks/ page, delimited by comma\")\ndefine(\"auth_provider\", default=None, type=str, help=\"auth handler class\")\ndefine(\"url_prefix\", type=str, help=\"base url prefix\")\ndefine(\"task_runtime_metric_buckets\", type=float, default=Histogram.DEFAULT_BUCKETS,\n       multiple=True, help=\"histogram latency bucket value\")\n\n\ndefault_options = options\n"
  },
  {
    "path": "flower/static/css/flower.css",
    "content": ".bg-green {\n  background-color: #f0ffeb;\n}\n\n.dataTables_wrapper {\n  border: 1px solid #c7ecb8;\n}\n\n.dataTables_filter input {\n  width: 50%;\n  text-indent: 5px;\n}\n\n.dataTables_length {\n  margin: 10px;\n}\n\ndiv.dataTables_wrapper .dataTables_filter input {\n  width: 100%;\n  margin: 10px;\n  border: 1px solid #c7ecb8;\n}\n\n.dataTables_info {\n  padding: 5px;\n}\n\n@media (min-width: 768px) {\n  div.dataTables_wrapper .dataTables_filter input {\n      width: 500px;\n  }\n}\n\n.overflow-auto {\n  max-width: 400px;\n  text-overflow: ellipsis;\n}\n\n.overflow-auto::-webkit-scrollbar {\n  display: none;\n}\n"
  },
  {
    "path": "flower/static/js/flower.js",
    "content": "/*jslint browser: true */\n/*global $, WebSocket, jQuery */\n\nvar flower = (function () {\n    \"use strict\";\n\n    var alertContainer = document.getElementById('alert-container');\n    function show_alert(message, type) {\n        var wrapper = document.createElement('div');\n        wrapper.innerHTML = `\n            <div class=\"alert alert-${type} alert-dismissible\" role=\"alert\">\n                <div>${message}</div>\n                <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>\n            </div>`;\n        alertContainer.appendChild(wrapper);\n    }\n\n    function url_prefix() {\n        var prefix = $('#url_prefix').val();\n        if (prefix) {\n            prefix = prefix.replace(/\\/+$/, '');\n            if (prefix.startsWith('/')) {\n                return prefix;\n            } else {\n                return '/' + prefix;\n            }\n        }\n        return '';\n    }\n\n    //https://github.com/DataTables/DataTables/blob/1.10.11/media/js/jquery.dataTables.js#L14882\n    function htmlEscapeEntities(d) {\n        return typeof d === 'string' ?\n            d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;') :\n            d;\n    }\n\n    function active_page(name) {\n        var pathname = $(location).attr('pathname');\n        if (name === '/') {\n            return pathname === (url_prefix() + name);\n        }\n        else {\n            return pathname.startsWith(url_prefix() + name);\n        }\n    }\n\n    $('#worker-refresh').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n        $('.dropdown-toggle').dropdown('hide');\n\n        var workername = $('#workername').text();\n\n        $.ajax({\n            type: 'GET',\n            url: url_prefix() + '/api/workers',\n            dataType: 'json',\n            data: {\n                workername: unescape(workername),\n                refresh: 1\n            },\n            success: function (data) {\n                show_alert(data.message || 'Successfully refreshed', 'success');\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-refresh-all').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n        $('.dropdown-toggle').dropdown('hide');\n\n        $.ajax({\n            type: 'GET',\n            url: url_prefix() + '/api/workers',\n            dataType: 'json',\n            data: {\n                refresh: 1\n            },\n            success: function (data) {\n                show_alert(data.message || 'Refreshed All Workers', 'success');\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-pool-restart').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n        $('.dropdown-toggle').dropdown('hide');\n\n        var workername = $('#workername').text();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/pool/restart/' + workername,\n            dataType: 'json',\n            data: {\n                workername: workername\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-shutdown').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n        $('.dropdown-toggle').dropdown('hide');\n\n        var workername = $('#workername').text();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/shutdown/' + workername,\n            dataType: 'json',\n            data: {\n                workername: workername\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-pool-grow').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var workername = $('#workername').text(),\n            grow_size = $('#pool-size').val();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/pool/grow/' + workername,\n            dataType: 'json',\n            data: {\n                'workername': workername,\n                'n': grow_size,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-pool-shrink').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var workername = $('#workername').text(),\n            shrink_size = $('#pool-size').val();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/pool/shrink/' + workername,\n            dataType: 'json',\n            data: {\n                'workername': workername,\n                'n': shrink_size,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-pool-autoscale').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var workername = $('#workername').text(),\n            min = $('#min-autoscale').val(),\n            max = $('#max-autoscale').val();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/pool/autoscale/' + workername,\n            dataType: 'json',\n            data: {\n                'workername': workername,\n                'min': min,\n                'max': max,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-add-consumer').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var workername = $('#workername').text(),\n            queue = $('#add-consumer-name').val();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/queue/add-consumer/' + workername,\n            dataType: 'json',\n            data: {\n                'workername': workername,\n                'queue': queue,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#worker-queues').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        if (!event.target.id.startsWith(\"worker-cancel-consumer\")) {\n            return;\n        }\n\n        var workername = $('#workername').text(),\n            queue = $(event.target).closest(\"tr\").children(\"td:eq(0)\").text();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/worker/queue/cancel-consumer/' + workername,\n            dataType: 'json',\n            data: {\n                'workername': workername,\n                'queue': queue,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#limits-table').on('click', function (event) {\n        if (event.target.id.startsWith(\"task-timeout-\")) {\n            var timeout = parseInt($(event.target).siblings().closest(\"input\").val()),\n                type = $(event.target).text().toLowerCase(),\n                taskname = $(event.target).closest(\"tr\").children(\"td:eq(0)\").text(),\n                post_data = {'workername': $('#workername').text()};\n\n            taskname = taskname.split(' ')[0]; // removes [rate_limit=xxx]\n            post_data[type] = timeout;\n\n            if (!Number.isInteger(timeout)) {\n                show_alert(\"Invalid timeout value\", \"danger\");\n                return;\n            }\n\n            $.ajax({\n                type: 'POST',\n                url: url_prefix() + '/api/task/timeout/' + taskname,\n                dataType: 'json',\n                data: post_data,\n                success: function (data) {\n                    show_alert(data.message, \"success\");\n                },\n                error: function (data) {\n                    show_alert($(data.responseText).text(), \"danger\");\n                }\n            });\n        } else if (event.target.id.startsWith(\"task-rate-limit-\")) {\n            var taskname = $(event.target).closest(\"tr\").children(\"td:eq(0)\").text(),\n                workername = $('#workername').text(),\n                ratelimit = parseInt($(event.target).prev().val());\n\n            taskname = taskname.split(' ')[0]; // removes [rate_limit=xxx]\n\n            $.ajax({\n                type: 'POST',\n                url: url_prefix() + '/api/task/rate-limit/' + taskname,\n                dataType: 'json',\n                data: {\n                    'workername': workername,\n                    'ratelimit': ratelimit,\n                },\n                success: function (data) {\n                    show_alert(data.message, \"success\");\n                },\n                error: function (data) {\n                    show_alert(data.responseText, \"danger\");\n                }\n            });\n        }\n    });\n\n    $('#task-revoke').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var taskid = $('#taskid').text();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/task/revoke/' + taskid,\n            dataType: 'json',\n            data: {\n                'terminate': false,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n                document.getElementById(\"task-revoke\").disabled = true;\n                setTimeout(function() {location.reload();}, 5000);\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    $('#task-terminate').on('click', function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var taskid = $('#taskid').text();\n\n        $.ajax({\n            type: 'POST',\n            url: url_prefix() + '/api/task/revoke/' + taskid,\n            dataType: 'json',\n            data: {\n                'terminate': true,\n            },\n            success: function (data) {\n                show_alert(data.message, \"success\");\n                document.getElementById(\"task-terminate\").disabled = true;\n                setTimeout(function() {location.reload();}, 5000);\n            },\n            error: function (data) {\n                show_alert(data.responseText, \"danger\");\n            }\n        });\n    });\n\n    function sum(a, b) {\n        return parseInt(a, 10) + parseInt(b, 10);\n    }\n\n    function format_time(timestamp) {\n        var time = $('#time').val(),\n            prefix = time.startsWith('natural-time') ? 'natural-time' : 'time',\n            tz = time.substr(prefix.length + 1) || 'UTC';\n\n        if (prefix === 'natural-time') {\n            return moment.unix(timestamp).tz(tz).fromNow();\n        }\n        return moment.unix(timestamp).tz(tz).format('YYYY-MM-DD HH:mm:ss.SSS');\n    }\n\n    function isColumnVisible(name) {\n        var columns = $('#columns').val();\n        if (columns === \"all\")\n            return true;\n        if (columns) {\n            columns = columns.split(',').map(function (e) {\n                return e.trim();\n            });\n            return columns.indexOf(name) !== -1;\n        }\n        return true;\n    }\n\n    $.urlParam = function (name) {\n        var results = new RegExp('[\\\\?&]' + name + '=([^&#]*)').exec(window.location.href);\n        return (results && results[1]) || 0;\n    };\n\n    $(document).ready(function () {\n        //https://github.com/twitter/bootstrap/issues/1768\n        var shiftWindow = function () {\n            scrollBy(0, -50);\n        };\n        if (location.hash) {\n            shiftWindow();\n        }\n        window.addEventListener(\"hashchange\", shiftWindow);\n\n        // Make bootstrap tabs persistent\n        $(document).ready(function () {\n            if (location.hash !== '') {\n                $('a[href=\"' + location.hash + '\"]').tab('show');\n            }\n\n            // Listen for tab shown events and update the URL hash fragment accordingly\n            $('.nav-tabs a[data-bs-toggle=\"tab\"]').on('shown.bs.tab', function (event) {\n                const tabPaneId = $(event.target).attr('href').substr(1);\n                if (tabPaneId) {\n                    window.location.hash = tabPaneId;\n                }\n            });\n        });\n    });\n\n    $(document).ready(function () {\n        if (!active_page('/') && !active_page('/workers')) {\n            return;\n        }\n\n        $('#workers-table').DataTable({\n            rowId: 'name',\n            searching: true,\n            select: false,\n            paging: true,\n            scrollCollapse: true,\n            lengthMenu: [15, 30, 50, 100],\n            pageLength: 15,\n            language: {\n                lengthMenu: 'Show _MENU_ workers',\n                info: 'Showing _START_ to _END_ of _TOTAL_ workers',\n                infoFiltered: '(filtered from _MAX_ total workers)'\n            },\n            ajax: url_prefix() + '/workers?json=1',\n            order: [\n                [1, \"des\"]\n            ],\n            footerCallback: function( tfoot, data, start, end, display ) {\n                var api = this.api();\n                var columns = {2:\"STARTED\", 3:\"\", 4:\"FAILURE\", 5:\"SUCCESS\", 6:\"RETRY\"};\n                for (const [column, state] of Object.entries(columns)) {\n                    var total = api.column(column).data().reduce(sum, 0);\n                    var footer = total;\n                    if (total !== 0) {\n                        let queryParams = (state !== '' ? `?state=${state}` : '');\n                        footer = '<a href=\"' + url_prefix() + '/tasks' + queryParams + '\">' + total + '</a>';\n                    }\n                    $(api.column(column).footer()).html(footer);\n                }\n            },\n            columnDefs: [{\n                targets: 0,\n                data: 'hostname',\n                type: 'natural',\n                render: function (data, type, full, meta) {\n                    return '<a href=\"' + url_prefix() + '/worker/' + encodeURIComponent(data) + '\">' + data + '</a>';\n                }\n            }, {\n                targets: 1,\n                data: 'status',\n                className: \"text-center\",\n                width: \"10%\",\n                render: function (data, type, full, meta) {\n                    if (data) {\n                        return '<span class=\"badge bg-success\">Online</span>';\n                    } else {\n                        return '<span class=\"badge bg-secondary\">Offline</span>';\n                    }\n                }\n            }, {\n                targets: 2,\n                data: 'active',\n                className: \"text-center\",\n                width: \"10%\",\n                defaultContent: 0\n            }, {\n                targets: 3,\n                data: 'task-received',\n                className: \"text-center\",\n                width: \"10%\",\n                defaultContent: 0\n            }, {\n                targets: 4,\n                data: 'task-failed',\n                className: \"text-center\",\n                width: \"10%\",\n                defaultContent: 0\n            }, {\n                targets: 5,\n                data: 'task-succeeded',\n                className: \"text-center\",\n                width: \"10%\",\n                defaultContent: 0\n            }, {\n                targets: 6,\n                data: 'task-retried',\n                className: \"text-center\",\n                width: \"10%\",\n                defaultContent: 0\n            }, {\n                targets: 7,\n                data: 'loadavg',\n                width: \"10%\",\n                className: \"text-center text-nowrap\",\n                render: function (data, type, full, meta) {\n                    if (!full.status) {\n                        return 'N/A';\n                    }\n                    if (Array.isArray(data)) {\n                        return data.join(', ');\n                    }\n                    return data;\n                }\n            }, ],\n        });\n\n        var autorefresh_interval = $.urlParam('autorefresh') || 1;\n        if (autorefresh !== 0) {\n            setInterval( function () {\n                $('#workers-table').DataTable().ajax.reload(null, false);\n            }, autorefresh_interval * 1000);\n        }\n\n    });\n\n    $(document).ready(function () {\n        if (!active_page('/tasks')) {\n            return;\n        }\n\n        $('#tasks-table').DataTable({\n            rowId: 'uuid',\n            searching: true,\n            scrollX: true,\n            scrollCollapse: true,\n            processing: true,\n            serverSide: true,\n            colReorder: true,\n            lengthMenu: [15, 30, 50, 100],\n            pageLength: 15,\n            stateSave: true,\n            language: {\n                lengthMenu: 'Show _MENU_ tasks',\n                info: 'Showing _START_ to _END_ of _TOTAL_ tasks',\n                infoFiltered: '(filtered from _MAX_ total tasks)'\n            },\n            ajax: {\n                type: 'POST',\n                url: url_prefix() + '/tasks/datatable'\n            },\n            order: [\n                [7, \"desc\"]\n            ],\n            oSearch: {\n                \"sSearch\": $.urlParam('state') ? 'state:' + $.urlParam('state') : ''\n            },\n            columnDefs: [{\n                targets: 0,\n                data: 'name',\n                visible: isColumnVisible('name'),\n                render: function (data, type, full, meta) {\n                    return data;\n                }\n            }, {\n                targets: 1,\n                data: 'uuid',\n                visible: isColumnVisible('uuid'),\n                orderable: false,\n                className: \"text-nowrap\",\n                render: function (data, type, full, meta) {\n                    return '<a href=\"' + url_prefix() + '/task/' + encodeURIComponent(data) + '\">' + data + '</a>';\n                }\n            }, {\n                targets: 2,\n                data: 'state',\n                visible: isColumnVisible('state'),\n                className: \"text-center\",\n                render: function (data, type, full, meta) {\n                    switch (data) {\n                    case 'SUCCESS':\n                        return '<span class=\"badge bg-success\">' + data + '</span>';\n                    case 'FAILURE':\n                        return '<span class=\"badge bg-danger\">' + data + '</span>';\n                    default:\n                        return '<span class=\"badge bg-secondary\">' + data + '</span>';\n                    }\n                }\n            }, {\n                targets: 3,\n                data: 'args',\n                className: \"text-nowrap overflow-auto\",\n                visible: isColumnVisible('args'),\n                render: htmlEscapeEntities\n            }, {\n                targets: 4,\n                data: 'kwargs',\n                className: \"text-nowrap overflow-auto\",\n                visible: isColumnVisible('kwargs'),\n                render: htmlEscapeEntities\n            }, {\n                targets: 5,\n                data: 'result',\n                visible: isColumnVisible('result'),\n                className: \"text-nowrap overflow-auto\",\n                render: htmlEscapeEntities\n            }, {\n                targets: 6,\n                data: 'received',\n                className: \"text-nowrap\",\n                visible: isColumnVisible('received'),\n                render: function (data, type, full, meta) {\n                    if (data) {\n                        return format_time(data);\n                    }\n                    return data;\n                }\n            }, {\n                targets: 7,\n                data: 'started',\n                className: \"text-nowrap\",\n                visible: isColumnVisible('started'),\n                render: function (data, type, full, meta) {\n                    if (data) {\n                        return format_time(data);\n                    }\n                    return data;\n                }\n            }, {\n                targets: 8,\n                data: 'runtime',\n                className: \"text-center\",\n                visible: isColumnVisible('runtime'),\n                render: function (data, type, full, meta) {\n                    return data ? data.toFixed(2) : data;\n                }\n            }, {\n                targets: 9,\n                data: 'worker',\n                visible: isColumnVisible('worker'),\n                render: function (data, type, full, meta) {\n                    return '<a href=\"' + url_prefix() + '/worker/' + encodeURIComponent(data) + '\">' + data + '</a>';\n                }\n            }, {\n                targets: 10,\n                data: 'exchange',\n                visible: isColumnVisible('exchange')\n            }, {\n                targets: 11,\n                data: 'routing_key',\n                visible: isColumnVisible('routing_key')\n            }, {\n                targets: 12,\n                data: 'retries',\n                className: \"text-center\",\n                visible: isColumnVisible('retries')\n            }, {\n                targets: 13,\n                data: 'revoked',\n                className: \"text-nowrap\",\n                visible: isColumnVisible('revoked'),\n                render: function (data, type, full, meta) {\n                    if (data) {\n                        return format_time(data);\n                    }\n                    return data;\n                }\n            }, {\n                targets: 14,\n                data: 'exception',\n                className: \"text-nowrap\",\n                visible: isColumnVisible('exception')\n            }, {\n                targets: 15,\n                data: 'expires',\n                visible: isColumnVisible('expires')\n            }, {\n                targets: 16,\n                data: 'eta',\n                visible: isColumnVisible('eta')\n            }, ],\n        });\n\n    });\n\n}(jQuery));\n"
  },
  {
    "path": "flower/static/swagger.json",
    "content": "{\n  \"tags\": [\n    \n  ],\n  \"paths\": {\n    \"\\/api\\/tasks\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Result\"\n          }\n        },\n        \"description\": \"List tasks\",\n        \"parameters\": [\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"the maximum number of tasks\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"workername\",\n            \"in\": \"query\",\n            \"description\": \"filter task by workername\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"taskname\",\n            \"in\": \"query\",\n            \"description\": \"filter task by taskname\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"state\",\n            \"in\": \"query\",\n            \"description\": \"filter task by state\",\n            \"required\": false,\n            \"type\": \"string\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/types\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"List (seen) task types\"\n      }\n    },\n    \"\\/api\\/queues\\/length\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Get queue lengths\"\n      }\n    },\n    \"\\/api\\/task\\/info\\/{taskid}\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Get task info\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskid\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/apply\\/{taskname}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Execute a task by name and wait results\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskname\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"kwargs\": {\n                  \"type\": \"object\"\n                },\n                \"args\": {\n                  \"type\": \"array\"\n                }\n              }\n            },\n            \"name\": \"args\",\n            \"description\": \"the dictionary of args and kwargs\",\n            \"in\": \"body\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/async-apply\\/{taskname}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Execute a task\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskname\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"kwargs\": {\n                  \"type\": \"object\"\n                },\n                \"args\": {\n                  \"type\": \"array\"\n                },\n                \"options\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"name\": \"args\",\n            \"description\": \"the dictionary of args, kwargs, and apply-async options\",\n            \"in\": \"body\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/send-task\\/{taskname}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Execute a task by name (Doesn't require a task source)\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskname\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"kwargs\": {\n                  \"type\": \"object\"\n                },\n                \"args\": {\n                  \"type\": \"array\"\n                }\n              }\n            },\n            \"name\": \"args\",\n            \"description\": \"the dictionary of args, and kwargs\",\n            \"in\": \"body\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/result\\/{taskid}\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Get a task result\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskid\"\n          },\n          {\n            \"name\": \"timeout\",\n            \"in\": \"query\",\n            \"description\": \"how long to wait, in seconds, before the operation times out\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/abort\\/{taskid}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Abort a running task\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskid\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/timeout\\/{taskname}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Change soft and hard time limits for a task\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskname\"\n          },\n          {\n            \"name\": \"workername\",\n            \"in\": \"query\",\n            \"description\": \"the name of a worker\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"soft\",\n            \"in\": \"query\",\n            \"description\": \"the soft timeout limit\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"hard\",\n            \"in\": \"query\",\n            \"description\": \"the hard timeout limit\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/rate-limit\\/{taskname}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Change rate limit for a task\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskname\"\n          },\n          {\n            \"name\": \"workername\",\n            \"in\": \"query\",\n            \"description\": \"the name of a worker\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"rateLimit\",\n            \"in\": \"query\",\n            \"description\": \"the rate limit to apply\",\n            \"required\": true,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/task\\/revoke\\/{taskid}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Revoke a task\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/taskid\"\n          },\n          {\n            \"name\": \"terminate\",\n            \"in\": \"query\",\n            \"description\": \"terminate the task if it is running\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/workers\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"List workers\",\n        \"parameters\": [\n          {\n            \"name\": \"refresh\",\n            \"in\": \"query\",\n            \"description\": \"run inspect to get updated list of workers\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          },\n          {\n            \"name\": \"workername\",\n            \"in\": \"query\",\n            \"description\": \"get info for workername\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"status\",\n            \"description\": \"only get worker status info\",\n            \"in\": \"query\",\n            \"type\": \"boolean\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/shutdown\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Shut down a worker\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/pool\\/restart\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Restart a worker's pool\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/pool\\/grow\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Grow a worker's pool\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          },\n          {\n            \"name\": \"n\",\n            \"in\": \"query\",\n            \"description\": \"number of pool processes to grow, default is 1\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/pool\\/shrink\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Shrink a worker's pool\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          },\n          {\n            \"name\": \"n\",\n            \"in\": \"query\",\n            \"description\": \"number of pool processes to shrink, default is 1\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/pool\\/autoscale\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Autoscale a worker pool\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          },\n          {\n            \"name\": \"min\",\n            \"in\": \"query\",\n            \"description\": \"minimum number of pool processes\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"max\",\n            \"in\": \"query\",\n            \"description\": \"maximum number of pool processes\",\n            \"required\": false,\n            \"format\": \"int32\",\n            \"type\": \"integer\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/queue\\/add-consumer\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Start consuming from a queue\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          },\n          {\n            \"name\": \"queue\",\n            \"in\": \"query\",\n            \"description\": \"the name of a queue\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ]\n      }\n    },\n    \"\\/api\\/worker\\/queue\\/cancel-consumer\\/{workername}\": {\n      \"post\": {\n        \"responses\": {\n          \"200\": {\n            \"description\": \"result\"\n          }\n        },\n        \"description\": \"Stop consuming from a queue\",\n        \"parameters\": [\n          {\n            \"$ref\": \"#\\/parameters\\/workername\"\n          },\n          {\n            \"name\": \"queue\",\n            \"in\": \"query\",\n            \"description\": \"the name of a queue\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ]\n      }\n    }\n  },\n  \"parameters\": {\n    \"taskid\": {\n      \"type\": \"string\",\n      \"in\": \"path\",\n      \"description\": \"The task id\",\n      \"required\": true,\n      \"name\": \"taskid\"\n    },\n    \"workername\": {\n      \"type\": \"string\",\n      \"in\": \"path\",\n      \"description\": \"The worker name\",\n      \"required\": true,\n      \"name\": \"workername\"\n    },\n    \"taskname\": {\n      \"type\": \"string\",\n      \"in\": \"path\",\n      \"description\": \"The task name\",\n      \"required\": true,\n      \"name\": \"taskname\"\n    }\n  },\n  \"info\": {\n    \"description\": \"The flower API spec\",\n    \"version\": \"1.0.0-dev\",\n    \"title\": \"Flower\"\n  },\n  \"definitions\": {\n    \n  },\n  \"swagger\": \"2.0\"\n}\n\n"
  },
  {
    "path": "flower/templates/404.html",
    "content": "{% extends \"base.html\" %}\n\n{% block container %}\n    <div class=\"col-12\">\n        <p>\n            {% if message %}\n                {{ message }}\n            {% else %}\n                Error, page not found\n            {% end %}\n        </p>\n    </div>\n{% end %}\n"
  },
  {
    "path": "flower/templates/base.html",
    "content": "{% import pprint %}\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Flower</title>\n  \t<link rel=\"stylesheet\" href=\"{{ static_url('css/bootstrap.min.css') }}\">\n\t  <link rel=\"stylesheet\" href=\"{{ static_url('css/datatables-1.13.4.min.css') }}\">\n\t  <link href=\"{{ static_url('css/flower.css') }}\" rel=\"stylesheet\">\n  </head>\n  <body class=\"m-2\">\n    {% block navbar %}\n      {% module Template(\"navbar.html\", active_tab=\"\") %}\n    {% end %}\n\n    <div class=\"container-fluid my-2\">\n      <div id=\"alert-container\"></div>\n      <input type=\"hidden\" value=\"{{ url_prefix or '' }}\" id='url_prefix'>\n    </div>\n\n    {% block container %}\n    {% end %}\n\n    <script src=\"{{ static_url('js/bootstrap.bundle.min.js') }}\"></script>\n    <script src=\"{{ static_url('js/jquery-3.6.4.min.js') }}\"></script>\n    <script src=\"{{ static_url('js/datatables-1.13.4.min.js') }}\"></script>\n    <script src=\"{{ static_url('js/moment-2.29.4.min.js') }}\"></script>\n    <script src=\"{{ static_url('js/moment-timezone-with-data-2.29.4.min.js') }}\"></script>\n\t  <script src=\"{{ static_url('js/flower.js') }}\"></script>\n\n    {% block extra_scripts %}\n    {% end %}\n  </body>\n</html>"
  },
  {
    "path": "flower/templates/broker.html",
    "content": "{% extends \"base.html\" %}\n\n{% block navbar %}\n{% module Template(\"navbar.html\", active_tab=\"broker\") %}\n{% end %}\n\n{% block container %}\n<div class=\"container-fluid\">\n  <figure class=\"table-responsive mt-3\">\n    <table id=\"queue-table\" class=\"table table-bordered table-striped caption-top\">\n      <caption>{{ broker_url }}</caption>\n      <thead>\n        <tr>\n          <th>Queue</th>\n          <th>Messages</th>\n          <th>Unacked</th>\n          <th>Ready</th>\n          <th>Consumers</th>\n          <th>Idle since</th>\n        </tr>\n      </thead>\n      <tbody>\n        {% for queue in queues %}\n        <tr id=\"{{ url_escape(queue['name']) }}\">\n          <td>{{ queue['name'] }}</td>\n          <td>{{ queue.get('messages', 'N/A') }}</td>\n          <td>{{ queue.get('messages_unacknowledged', 'N/A') }}</td>\n          <td>{{ queue.get('messages_ready', 'N/A') }}</td>\n          <td>{{ queue.get('consumers', 'N/A') }}</td>\n          <td>{{ queue.get('idle_since', 'N/A') }}</td>\n        </tr>\n        {% end %}\n      </tbody>\n    </table>\n  </figure>\n</div>\n{% end %}"
  },
  {
    "path": "flower/templates/error.html",
    "content": "{% extends \"base.html\" %}\n\n{% block container %}\n    {% if debug %}\n    <div class=\"col-12\">\n        <p>It looks like you have found a bug! You can help to improve\n        Flower by opening an issue in <a href=\"https://github.com/mher/flower/issues\">https://github.com/mher/flower/issues</a>\n        </p>\n        <pre>\n{{ bugreport }}\n\n{{ error_trace }}\n        </pre>\n    </div>\n    {% else %}\n    <div class=\"col-12\">\n        Error {{ status_code }}\n    </div>\n    {% end %}\n{% end %}\n"
  },
  {
    "path": "flower/templates/navbar.html",
    "content": "<nav class=\"navbar navbar-expand-lg navbar-light bg-green mx-2\">\n  <a class=\"navbar-brand\" href=\"{{ reverse_url('main') }}\">\n    <img src=\"{{ static_url('favicon.ico') }}\" width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"\">\n    Flower\n  </a>\n  <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarSupportedContent\"\n    aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n    <span class=\"navbar-toggler-icon\"></span>\n  </button>\n\n  <div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\">\n    <ul class=\"navbar-nav mr-auto\">\n      <li class=\"nav-item active\">\n        <a class=\"nav-link text-dark\" href=\"{{ reverse_url('workers') }}\">Workers</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link text-dark\" href=\"{{ reverse_url('tasks') }}\">Tasks</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link text-dark\" href=\"{{ reverse_url('broker') }}\">Broker</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link text-dark\" href=\"https://flower.readthedocs.io/\" target=\"_blank\" rel=\"noopener\">Documentation</a>\n      </li>\n    </ul>\n\n    <ul class=\"navbar-nav flex-row flex-wrap ms-md-auto\">\n      <li class=\"nav-item col-6 col-lg-auto\">\n        <a class=\"nav-link py-2 px-0 px-lg-2\" href=\"https://github.com/mher/flower\" target=\"_blank\" rel=\"noopener\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"30\" height=\"30\" class=\"navbar-nav-svg\" viewBox=\"0 0 512 499.36\"\n            role=\"img\">\n            <path fill=\"currentColor\" fill-rule=\"evenodd\"\n              d=\"M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z\" />\n          </svg>\n\n          <small class=\"d-lg-none ms-2\">GitHub</small>\n        </a>\n      </li>\n    </ul>\n  </div>\n</nav>"
  },
  {
    "path": "flower/templates/task.html",
    "content": "{% extends \"base.html\" %}\n\n{% block navbar %}\n  {% module Template(\"navbar.html\", active_tab=\"tasks\") %}\n{% end %}\n\n{% block container %}\n  <div id='task-page' class=\"container-fluid mt-3\">\n    <div class=\"row-fluid\">\n      <div class=\"col-lg-12\">\n        <div class=\"page-header\">\n          <p id=\"taskid\" class=\"d-none\">{{ task.uuid }}</p>\n          <h2>{{ getattr(task, 'name', None) }}\n            <small class=\"text-muted fs-5\">{{ task.uuid }}</small>\n            {% if task.state == \"STARTED\" %}\n                <button class=\"btn btn-danger float-end\" id=\"task-terminate\">Terminate</button>\n            {% elif task.state == \"RECEIVED\" or task.state == \"RETRY\" %}\n                <button class=\"btn btn-danger float-end\" id=\"task-revoke\">Revoke</button>\n            {% end %}\n          </h2>\n        </div>\n        <div class=\"row-fluid\">\n          <div class=\"col-lg-6\">\n            <table class=\"table table-bordered table-striped\">\n              <tbody>\n              <tr>\n                <td>Name</td>\n                <td>{{ getattr(task, 'name', None) }}</td>\n              </tr>\n              <tr>\n                <td>UUID</td>\n                <td>{{ task.uuid }}</td>\n              </tr>\n              <tr>\n                <td>State</td>\n                <td>\n                  {% if task.state == \"SUCCESS\" %}\n                  <span class=\"badge bg-success\">{{ task.state }}</span>\n                  {% elif task.state == \"FAILURE\" %}\n                  <span class=\"badge bg-danger\">{{ task.state }}</span>\n                  {% else %}\n                  <span class=\"badge bg-secondary\">{{ task.state }}</span>\n                  {% end %}\n                </td>\n              </tr>\n              <tr>\n                <td>args</td>\n                <td>{{ task.args }}</td>\n              </tr>\n              <tr>\n                <td>kwargs</td>\n                <td>{{ task.kwargs }}</td>\n              </tr>\n              <tr>\n                <td>Result</td>\n                <td>{{ getattr(task, 'result', '') }}</td>\n              </tr>\n              {% for name in task._fields %}\n                {% if name not in ['name', 'uuid', 'state', 'args', 'kwargs', 'result'] and getattr(task, name, None) is not None %}\n                <tr>\n                  <td>{{ humanize(name) }}</td>\n                  <td>\n                    {% if name in ['sent', 'received', 'started', 'succeeded', 'retried', 'timestamp', 'failed', 'revoked'] %}\n                    {{ humanize(getattr(task, name, None), type='time') }}\n                    {% elif name == 'worker' %}\n                    <a\n                        href=\"{{ reverse_url('worker', task.worker.hostname) }}\">{{ task.worker.hostname }}</a>\n                    {% elif name == 'traceback' %}\n                    <pre>{{ getattr(task, name, None) }}</pre>\n                    {% elif name in ['parent_id', 'root_id'] %}\n                    <a\n                        href=\"{{ reverse_url('task', getattr(task, name, None)) }}\">{{ getattr(task, name, None) }}</a>\n                    {% elif name == 'children' %}\n                      {% for child in getattr(task, name, {}) %}\n                        <a href=\"{{ reverse_url('task', child.id) }}\">{{ child.id }}</a>\n                        <br>\n                      {% end %}\n                    {% else %}\n                      {{ getattr(task, name, None) }}\n                    {% end %}\n                  </td>\n                </tr>\n                {% end %}\n              {% end %}\n              </tbody>\n            </table>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n{% end %}\n"
  },
  {
    "path": "flower/templates/tasks.html",
    "content": "{% extends \"base.html\" %}\n\n{% block navbar %}\n  {% module Template(\"navbar.html\", active_tab=\"tasks\") %}\n{% end %}\n\n\n{% block container %}\n<input type=\"hidden\" value=\"{{ time }}\" id='time'>\n<input type=\"hidden\" value=\"{{ columns }}\" id='columns'>\n\n<div class=\"container-fluid mt-3\">\n  <table id=\"tasks-table\" class=\"table table-bordered table-striped table-hover w-100\">\n    <thead>\n    <tr>\n      <th>Name</th>\n      <th>UUID</th>\n      <th class=\"text-center\">State</th>\n      <th>args</th>\n      <th>kwargs</th>\n      <th>Result</th>\n      <th class=\"text-center\">Received</th>\n      <th class=\"text-center\">Started</th>\n      <th class=\"text-center\">Runtime</th>\n      <th>Worker</th>\n      <th>Exchange</th>\n      <th>Routing Key</th>\n      <th class=\"text-center\">Retries</th>\n      <th class=\"text-center\">Revoked</th>\n      <th>Exception</th>\n      <th class=\"text-center\">Expires</th>\n      <th class=\"text-center\">ETA</th>\n    </tr>\n    </thead>\n    <tbody>\n    {% for uuid, task in tasks %}\n        {% if getattr(task, 'name', None) is None %}\n            {% continue %}\n        {% end %}\n    <tr>\n      <td>{{ task.name }}</td>\n      <td>{{ task.uuid }}</td>\n      <td>{{ task.state }}</td>\n      <td>{{ task.args }}</td>\n      <td>{{ task.kwargs }}</td>\n      <td>\n        {% if task.state == \"SUCCESS\" %}\n            {{ task.result }}\n        {% elif task.state == \"FAILURE\" %}\n            {{ task.exception }}\n        {% end %}\n      </td>\n      <td>{{ humanize(task.received, type='time') }}</td>\n      <td>{{ humanize(task.started, type='time') }}</td>\n      <td>\n        {% if task.timestamp and task.started %}\n            {{ '%.2f' % humanize(task.timestamp - task.started) }} sec\n        {% end %}\n      </td>\n      <td>{{ task.worker }}</td>\n      <td>{{ task.exchange }}</td>\n      <td>{{ task.routing_key }}</td>\n      <td>{{ task.retries }}</td>\n      <td>{{ humanize(task.revoked, type='time') }}</td>\n      <td>{{ task.exception }}</td>\n      <td>{{ task.expires }}</td>\n      <td>{{ task.eta }}</td>\n    </tr>\n      {% end %}\n    </tbody>\n  </table>\n</div>\n{% end %}\n"
  },
  {
    "path": "flower/templates/worker.html",
    "content": "{% extends \"base.html\" %}\n\n{% block navbar %}\n{% module Template(\"navbar.html\", active_tab=\"workers\") %}\n{% end %}\n\n{% block container %}\n\n{% set other = {key: value for key, value in worker['stats'].items() if key not in\n'pool pid prefetch_count autoscaler consumer broker clock total rusage'.split()} %}\n\n<div class=\"container-fluid\">\n  <div class=\"row-fluid\">\n    <div class=\"col-lg-12\">\n      <div class=\"mt-4 mb-4\">\n        <h3 id=\"workername\">{{ worker['name'] }}</h3>\n      </div>\n\n      <div class=\"btn-group float-end\" role=\"group\" aria-label=\"Button Group\">\n        <button id=\"worker-group\" type=\"button\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\"\n          aria-expanded=\"false\">\n          Refresh\n        </button>\n        <ul class=\"dropdown-menu\" aria-labelledby=\"btnGroupDrop\">\n          <li><a id=\"worker-shutdown\" class=\"dropdown-item\" data-bs-dismiss=\"dropdown\">Shut Down</a></li>\n          <li><a id=\"worker-pool-restart\" class=\"dropdown-item\" data-bs-dismiss=\"dropdown\">Restart Pool</a></li>\n          <li><a id=\"worker-refresh\" class=\"dropdown-item\" data-bs-dismiss=\"dropdown\">Refresh</a></li>\n          <li><a id=\"worker-refresh-all\" class=\"dropdown-item\" data-bs-dismiss=\"dropdown\">Refresh All</a></li>\n        </ul>\n      </div>\n\n      <div class=\"tabbable\">\n        <ul class=\"nav nav-tabs\" role=\"tablist\">\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link active\" href=\"#tab-pool\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-pool\" type=\"button\" role=\"tab\" aria-controls=\"tab-pool\" aria-selected=\"true\">Pool</a>\n          </li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-broker\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-broker\" type=\"button\" role=\"tab\" aria-controls=\"tab-broker\"\n              aria-selected=\"false\">Broker</a></li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-queues\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-queues\" type=\"button\" role=\"tab\" aria-controls=\"tab-queues\"\n              aria-selected=\"false\">Queues</a></li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-tasks\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-tasks\" type=\"button\" role=\"tab\" aria-controls=\"tab-tasks\"\n              aria-selected=\"false\">Tasks</a></li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-limits\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-limits\" type=\"button\" role=\"tab\" aria-controls=\"tab-limits\"\n              aria-selected=\"false\">Limits</a></li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-config\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-config\" type=\"button\" role=\"tab\" aria-controls=\"tab-config\"\n              aria-selected=\"false\">Config</a></li>\n          <li class=\"nav-item\" role=\"presentation\"><a class=\"nav-link\" href=\"#tab-system\" data-bs-toggle=\"tab\"\n              data-bs-target=\"#tab-system\" type=\"button\" role=\"tab\" aria-controls=\"tab-system\"\n              aria-selected=\"false\">System</a></li>\n          {% if other %}\n          <li class=\"nav-item\"><a class=\"nav-link\" href=\"#tab-other\" data-bs-toggle=\"tab\" data-bs-target=\"#tab-other\"\n              type=\"button\" role=\"tab\" aria-controls=\"tab-other\" aria-selected=\"false\">Other</a></li>\n          {% end %}\n        </ul>\n\n        <div class=\"tab-content\">\n          <div class=\"tab-pane fade show active\" id=\"tab-pool\" role=\"tabpanel\" aria-labelledby=\"tab-pool\">\n            <div class=\"container-fluid\">\n              <div class=\"row\">\n                <div class=\"col-lg-8\">\n                  <table class=\"table table-bordered table-striped caption-top\">\n                    <caption>Worker pool options</caption>\n                    <tbody>\n                      {% for name,value in worker['stats'].get('pool', {}).items() %}\n                      <tr>\n                        <td>{{ humanize(name) }}</td>\n                        <td>{{ humanize(value) }}</td>\n                      </tr>\n                      {% end %}\n                      <tr>\n                        <td>Worker PID</td>\n                        <td>{{ worker['stats'].get('pid', 'N/A')}}</td>\n                      </tr>\n                      <tr>\n                        <td>Prefetch Count</td>\n                        <td>{{ worker['stats'].get('prefetch_count', 'N/A')}}</td>\n                      </tr>\n                    </tbody>\n                  </table>\n                </div>\n\n                <div class=\"col-lg-4 container\">\n                  <form class=\"mx-auto\">\n                    <legend class=\"form-label mt-md-5\">Pool size control</legend>\n                    <div class=\"mb-3\">\n                      <label for=\"pool-size\" class=\"col-sm-2 col-form-label text-nowrap\">Pool size</label>\n                      <input type=\"number\" id=\"pool-size\" min=\"1\" max=\"100\" value=\"1\">\n                      <button id=\"worker-pool-grow\" class=\"btn btn-primary btn-sm\" type=\"button\">Grow</button>\n                      <button id=\"worker-pool-shrink\" class=\"btn btn-primary btn-sm\" type=\"button\">Shrink</button>\n                    </div>\n                    <div class=\"mb-3 input-group\">\n                      <label for=\"min-autoscale\" class=\"col-sm-2 form-label text-nowrap\">Auto scale</label>\n                      <input class=\"form-control-sm\" id=\"min-autoscale\" type=\"number\" placeholder=\"Min\" min=\"1\"\n                        max=\"100\">\n                      <input class=\"form-control-sm mx-1\" id=\"max-autoscale\" type=\"number\" placeholder=\"Max\" min=\"1\"\n                        max=\"100\">\n                      <button id=\"worker-pool-autoscale\" class=\"btn btn-primary btn-sm\" type=\"button\">Apply</button>\n                    </div>\n                  </form>\n                </div>\n              </div>\n\n              {% if worker['stats'].get('autoscaler', None) %}\n              <div class=\"col-md-4\">\n                <table class=\"table table-bordered table-striped caption-top\">\n                  <caption>Autoscaler options</caption>\n                  <tbody>\n                    {% for name,value in worker['stats']['autoscaler'].items() %}\n                    <tr>\n                      <td>{{ humanize(name) }}</td>\n                      <td>{{ humanize(value) }}</td>\n                    </tr>\n                    {% end %}\n                  </tbody>\n                </table>\n              </div>\n              {% end %}\n            </div>\n          </div> <!-- end pool tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-broker\" role=\"tabpanel\" aria-labelledby=\"tab-broker\">\n            <div class=\"col-lg-6\">\n              <table class=\"table table-bordered table-striped caption-top\">\n                <caption>Broker options</caption>\n                <tbody>\n                  {% for name,value in (worker['stats'].get('consumer', None) or worker['stats'])['broker'].items() %}\n                  <tr>\n                    <td>{{ humanize(name) }}</td>\n                    <td>{{ value }}</td>\n                  </tr>\n                  {% end %}\n                </tbody>\n              </table>\n            </div>\n          </div> <!-- end broker tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-queues\" role=\"tabpanel\" aria-labelledby=\"tab-queues\">\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Active queues</caption>\n              <thead>\n                <tr>\n                  <th>Name</th>\n                  <th>Exclusive</th>\n                  <!-- <th>Exchange</th> -->\n                  <th>Durable</th>\n                  <th>Routing key</th>\n                  <th>No ACK</th>\n                  <th>Alias</th>\n                  <th>Queue arguments</th>\n                  <th>Binding arguments</th>\n                  <th>Auto delete</th>\n                  <th style=\"width: 125px;\"></th>\n                </tr>\n              </thead>\n              <tbody id=\"worker-queues\">\n                {% for queue in worker.get('active_queues', []) %}\n                <tr>\n                  <td>{{ queue['name'] }}</td>\n                  <td>{{ queue['exclusive'] }}</td>\n                  <!-- <td>{{ queue['exchange'] }}</td> -->\n                  <td>{{ queue['durable'] }}</td>\n                  <td>{{ queue['routing_key'] }}</td>\n                  <td>{{ queue['no_ack'] }}</td>\n                  <td>{{ queue['alias'] }}</td>\n                  <td>{{ queue['queue_arguments'] }}</td>\n                  <td>{{ queue['binding_arguments'] }}</td>\n                  <td>{{ queue['auto_delete'] }}</td>\n                  <td><button id=\"worker-cancel-consumer-{{ queue['name'] }}\" class=\"btn btn-danger text-nowrap\">Cancel\n                      Consumer</button></td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n            <div class=\"control-group col-lg-3\">\n              <div class=\"input-group mb-3\">\n                <input id=\"add-consumer-name\" type=\"text\" class=\"form-control\" placeholder=\"New consumer\"\n                  aria-label=\"New consumer\" aria-describedby=\"worker-add-consumer\">\n                <button class=\"btn btn-primary mx-1\" type=\"button\" id=\"worker-add-consumer\">Add</button>\n              </div>\n            </div>\n          </div> <!-- end queues tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-tasks\" role=\"tabpanel\" aria-labelledby=\"tab-tasks\">\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Processed tasks</caption>\n              <tbody>\n                {% for name,value in worker['stats']['total'].items() %}\n                <tr>\n                  <td>{{ name }}</td>\n                  <td>{{ value }}</td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Active tasks</caption>\n              <thead>\n                <tr>\n                  <th>Name</th>\n                  <th>UUID</th>\n                  <th>Ack</th>\n                  <th>PID</th>\n                  <th>args</th>\n                  <th>kwargs</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for task in worker.get('active', {}) %}\n                <tr>\n                  <td>{{ task['name'] }}</td>\n                  <td><a href=\"{{ reverse_url('task', task['id']) }}\">{{ task['id'] }}</a></td>\n                  <td>{{ task['acknowledged'] }}</td>\n                  <td>{{ task['worker_pid'] }}</td>\n                  <td>{{ task.get('args', 'N/A') }}</td>\n                  <td>{{ task.get('kwargs', 'N/A') }}</td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Scheduled tasks</caption>\n              <thead>\n                <tr>\n                  <th>Name</th>\n                  <th>UUID</th>\n                  <th>args</th>\n                  <th>kwargs</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for task in worker.get('scheduled', {}) %}\n                <tr>\n                  <td>{{ task['request']['name'] }}</td>\n                  <td><a href=\"{{ reverse_url('task', task['request']['id']) }}\">{{ task['request']['id'] }}</a></td>\n                  <td>{{ task['request']['args'] }}</td>\n                  <td>{{ task['request']['kwargs'] }}</td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Reserved tasks</caption>\n              <thead>\n                <tr>\n                  <th>Name</th>\n                  <th>UUID</th>\n                  <th>args</th>\n                  <th>kwargs</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for task in worker.get('reserved', {}) %}\n                <tr>\n                  <td>{{ task['name'] }}</td>\n                  <td><a href=\"{{ reverse_url('task', task['id']) }}\">{{ task['id'] }}</a></td>\n                  <td>{{ task['args'] }}</td>\n                  <td>{{ task['kwargs'] }}</td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n\n            <table class=\"table table-bordered table-striped caption-top\">\n              <caption>Revoked tasks</caption>\n              <thead>\n                <tr>\n                  <th>UUID</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for task in worker.get('revoked', []) %}\n                <tr>\n                  <td><a href=\"{{ reverse_url('task', task) }}\">{{ task }}</a></td>\n                </tr>\n                {% end %}\n              </tbody>\n            </table>\n          </div> <!-- end tasks tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-limits\" role=\"tabpanel\" aria-labelledby=\"tab-limits\">\n            <div class=\"col-lg-10\">\n              <table class=\"table table-bordered table-striped caption-top\">\n                <caption>Task limits</caption>\n                <thead>\n                  <tr>\n                    <th>Task</th>\n                    <th class=\"text-center\">Rate limit</th>\n                    <th class=\"text-center\">Timeouts</th>\n                  </tr>\n                </thead>\n                <tbody id=\"limits-table\">\n                  {% for taskname in worker.get('registered', []) %}\n                  <tr>\n                    <td>{{ taskname }}</td>\n                    <td class=\"col-lg-2\">\n                      <div class=\"form-group\">\n                        <div class=\"input-group\">\n                          <input class=\"form-control form-control-sm\" type=\"number\">\n                          <button class=\"btn btn-primary btn-sm mx-1\" type=\"button\"\n                            id=\"task-rate-limit-{{taskname}}\">Apply</button>\n                        </div>\n                      </div>\n                    </td>\n                    <td class=\"col-lg-2\">\n                      <div class=\"form-group\">\n                        <div class=\"input-group\">\n                          <input class=\"form-control form-control-sm\" type=\"number\">\n                          <button class=\"btn btn-primary btn-sm mx-1\" type=\"button\"\n                            id=\"task-timeout-soft-{{taskname}}\">Soft</button>\n                          <button class=\"btn btn-primary btn-sm mx-1\" type=\"button\"\n                            id=\"task-timeout-hard-{{taskname}}\">Hard</button>\n                        </div>\n                      </div>\n                    </td>\n                  </tr>\n                  {% end %}\n                </tbody>\n              </table>\n            </div>\n          </div> <!-- end limits tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-config\" role=\"tabpanel\" aria-labelledby=\"tab-config\">\n            <div class=\"col-lg-8\">\n              <table class=\"table table-bordered table-striped caption-top\">\n                <caption>Configuration options</caption>\n                <tbody>\n                  {% for name,value in sorted(worker.get('conf', {}).items()) %}\n                  {% if value is not None %}\n                  <tr>\n                    <td><a\n                        href=\"https://docs.celeryq.dev/en/latest/userguide/configuration.html#{{ name.lower().replace('_', '-') }}\"\n                        target=\"_blank\">{{ name }}</a></td>\n                    <td>{{ value }}</td>\n                  </tr>\n                  {% end %}\n                  {% end %}\n                </tbody>\n              </table>\n            </div>\n          </div> <!-- end config tab -->\n\n          <div class=\"tab-pane fade\" id=\"tab-system\" role=\"tabpanel\" aria-labelledby=\"tab-system\">\n            <div class=\"col-lg-8\">\n              <table class=\"table table-bordered table-striped caption-top\">\n                <caption>System usage statistics</caption>\n                <tbody>\n                  {% if isinstance(worker['stats'].get('rusage', None), dict) %}\n                  {% for name, value in worker['stats']['rusage'].items() %}\n                  <tr>\n                    <td>{{ name }}</td>\n                    <td>{{ value }}</td>\n                  </tr>\n                  {% end %}\n                  {% end %}\n                </tbody>\n              </table>\n            </div>\n          </div> <!-- end system tab -->\n\n          {% if other %}\n          <div class=\"tab-pane fade\" id=\"tab-other\" role=\"tabpanel\" aria-labelledby=\"tab-other\">\n            <div class=\"col-lg-8\">\n              <table class=\"table table-bordered table-striped caption-top\">\n                <caption>Other statistics</caption>\n                <tbody>\n                  {% for name, value in other.items() %}\n                  <tr>\n                    <td>{{ name }}</td>\n                    <td>{{ value }}</td>\n                  </tr>\n                  {% end %}\n                </tbody>\n              </table>\n            </div>\n          </div> <!-- end other tab -->\n          {% end %}\n\n        </div>\n      </div>\n    </div>\n  </div>\n  {% end %}"
  },
  {
    "path": "flower/templates/workers.html",
    "content": "{% extends \"base.html\" %}\n\n{% block navbar %}\n{% module Template(\"navbar.html\", active_tab=\"workers\")%}\n{% end %}\n\n{% block container %}\n<div class=\"container-fluid mt-3\">\n  <figure class=\"table-responsive\">\n    <table id=\"workers-table\" class=\"table table-bordered table-striped table-hover w-100\">\n      <thead>\n        <tr>\n          <th>Worker</th>\n          <th class=\"text-center\">Status</th>\n          <th class=\"text-center\">Active</th>\n          <th class=\"text-center\">Processed</th>\n          <th class=\"text-center\">Failed</th>\n          <th class=\"text-center\">Succeeded</th>\n          <th class=\"text-center\">Retried</th>\n          <th class=\"text-center\">Load Average</th>\n        </tr>\n      </thead>\n      <tbody>\n        {% for name, info in workers.items() %}\n        <tr id=\"{{ url_escape(name) }}\">\n          <td>{{ name }}</td>\n          <td>{{ info.get('status', None) }}</td>\n          <td>{{ info.get('active', 0) or 0 }}</td>\n          <td>{{ info.get('task-received', 0) }}</td>\n          <td>{{ info.get('task-failed', 0) }}</td>\n          <td>{{ info.get('task-succeeded', 0) }}</td>\n          <td>{{ info.get('task-retried', 0) }}</td>\n          <td>{{ humanize(info.get('loadavg', 'N/A')) }}</td>\n        </tr>\n        {% end %}\n      </tbody>\n      <tfoot>\n        <tr>\n          <th>Total</th>\n          <th></th>\n          <th></th>\n          <th></th>\n          <th></th>\n          <th></th>\n          <th></th>\n          <th></th>\n        </tr>\n      </tfoot>\n    </table>\n  </figure>\n</div>\n{% end %}\n\n{% block extra_scripts %}\n<script type=\"text/javascript\">\n  var autorefresh = {{ autorefresh }};\n</script>\n{% end %}\n"
  },
  {
    "path": "flower/urls.py",
    "content": "import os\n\nfrom tornado.web import StaticFileHandler, url\n\nfrom .api import control, tasks, workers\nfrom .utils import gen_cookie_secret\nfrom .views import auth, monitor\nfrom .views.broker import BrokerView\nfrom .views.error import NotFoundErrorHandler\nfrom .views.tasks import TasksDataTable, TasksView, TaskView\nfrom .views.workers import WorkersView, WorkerView\n\nsettings = dict(\n    template_path=os.path.join(os.path.dirname(__file__), \"templates\"),\n    static_path=os.path.join(os.path.dirname(__file__), \"static\"),\n    cookie_secret=gen_cookie_secret(),\n    static_url_prefix='/static/',\n    login_url='/login',\n)\n\n\nhandlers = [\n    # App\n    url(r\"/\", WorkersView, name='main'),\n    url(r\"/workers\", WorkersView, name='workers'),\n    url(r\"/worker/(.+)\", WorkerView, name='worker'),\n    url(r\"/task/(.+)\", TaskView, name='task'),\n    url(r\"/tasks\", TasksView, name='tasks'),\n    url(r\"/tasks/datatable\", TasksDataTable),\n    url(r\"/broker\", BrokerView, name='broker'),\n    # Worker API\n    (r\"/api/workers\", workers.ListWorkers),\n    (r\"/api/worker/shutdown/(.+)\", control.WorkerShutDown),\n    (r\"/api/worker/pool/restart/(.+)\", control.WorkerPoolRestart),\n    (r\"/api/worker/pool/grow/(.+)\", control.WorkerPoolGrow),\n    (r\"/api/worker/pool/shrink/(.+)\", control.WorkerPoolShrink),\n    (r\"/api/worker/pool/autoscale/(.+)\", control.WorkerPoolAutoscale),\n    (r\"/api/worker/queue/add-consumer/(.+)\", control.WorkerQueueAddConsumer),\n    (r\"/api/worker/queue/cancel-consumer/(.+)\",\n        control.WorkerQueueCancelConsumer),\n    # Task API\n    (r\"/api/tasks\", tasks.ListTasks),\n    (r\"/api/task/types\", tasks.ListTaskTypes),\n    (r\"/api/queues/length\", tasks.GetQueueLengths),\n    (r\"/api/task/info/(.*)\", tasks.TaskInfo),\n    (r\"/api/task/apply/(.+)\", tasks.TaskApply),\n    (r\"/api/task/async-apply/(.+)\", tasks.TaskAsyncApply),\n    (r\"/api/task/send-task/(.+)\", tasks.TaskSend),\n    (r\"/api/task/result/(.+)\", tasks.TaskResult),\n    (r\"/api/task/abort/(.+)\", tasks.TaskAbort),\n    (r\"/api/task/timeout/(.+)\", control.TaskTimout),\n    (r\"/api/task/rate-limit/(.+)\", control.TaskRateLimit),\n    (r\"/api/task/revoke/(.+)\", control.TaskRevoke),\n    # Metrics\n    (r\"/metrics\", monitor.Metrics),\n    (r\"/healthcheck\", monitor.Healthcheck),\n    # Static\n    (r\"/static/(.*)\", StaticFileHandler,\n     {\"path\": settings['static_path']}),\n    # Auth\n    (r\"/login\", auth.LoginHandler),\n\n    # Error\n    (r\".*\", NotFoundErrorHandler),\n]\n"
  },
  {
    "path": "flower/utils/__init__.py",
    "content": "import base64\nimport os.path\nimport uuid\n\nfrom .. import __version__\n\n\ndef gen_cookie_secret():\n    return base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)\n\n\ndef bugreport(app=None):\n    try:\n        import celery\n        import humanize\n        import tornado\n\n        app = app or celery.Celery()\n\n\t\t# pylint: disable=consider-using-f-string\n        return 'flower   -> flower:%s tornado:%s humanize:%s%s' % (\n            __version__,\n            tornado.version,\n            getattr(humanize, '__version__', None) or getattr(humanize, 'VERSION'),\n            app.bugreport()\n        )\n    except (ImportError, AttributeError) as e:\n        return f\"Error when generating bug report: {e}. Have you installed correct versions of Flower's dependencies?\"\n\n\ndef abs_path(path):\n    path = os.path.expanduser(path)\n    if not os.path.isabs(path):\n        cwd = os.environ.get('PWD') or os.getcwd()\n        path = os.path.join(cwd, path)\n    return path\n\n\ndef prepend_url(url, prefix):\n    return '/' + prefix.strip('/') + url\n\n\ndef strtobool(val):\n    \"\"\"Convert a string representation of truth to true (1) or false (0).\n\n    True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values\n    are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if\n    'val' is anything else.\n    \"\"\"\n    val = val.lower()\n    if val in ('y', 'yes', 't', 'true', 'on', '1'):\n        return 1\n    if val in ('n', 'no', 'f', 'false', 'off', '0'):\n        return 0\n    raise ValueError(f\"invalid truth value {val!r}\")\n"
  },
  {
    "path": "flower/utils/broker.py",
    "content": "import asyncio\nimport json\nimport logging\nimport numbers\nimport socket\nimport sys\nfrom urllib.parse import quote, unquote, urljoin, urlparse\n\nfrom tornado import httpclient, ioloop\n\ntry:\n    import redis\nexcept ImportError:\n    redis = None\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass BrokerBase:\n    def __init__(self, broker_url, *_, **__):\n        purl = urlparse(broker_url)\n        self.host = purl.hostname\n        self.port = purl.port\n        self.vhost = purl.path[1:]\n\n        username = purl.username\n        password = purl.password\n\n        self.username = unquote(username) if username else username\n        self.password = unquote(password) if password else password\n\n    async def queues(self, names):\n        raise NotImplementedError\n\n\nclass RabbitMQ(BrokerBase):\n    def __init__(self, broker_url, http_api, io_loop=None, **__):\n        super().__init__(broker_url)\n        self.io_loop = io_loop or ioloop.IOLoop.instance()\n\n        self.host = self.host or 'localhost'\n        self.port = self.port or 15672\n        self.vhost = quote(self.vhost, '') or '/' if self.vhost != '/' else self.vhost\n        self.username = self.username or 'guest'\n        self.password = self.password or 'guest'\n\n        if not http_api:\n            http_api = f\"http://{self.username}:{self.password}@{self.host}:{self.port}/api/{self.vhost}\"\n\n        try:\n            self.validate_http_api(http_api)\n        except ValueError:\n            logger.error(\"Invalid broker api url: %s\", http_api)\n\n        self.http_api = http_api\n\n    async def queues(self, names):\n        url = urljoin(self.http_api, 'queues/' + self.vhost)\n        api_url = urlparse(self.http_api)\n        username = unquote(api_url.username or '') or self.username\n        password = unquote(api_url.password or '') or self.password\n\n        http_client = httpclient.AsyncHTTPClient()\n        try:\n            response = await http_client.fetch(\n                url, auth_username=username, auth_password=password,\n                connect_timeout=1.0, request_timeout=2.0,\n                validate_cert=False)\n        except (socket.error, httpclient.HTTPError) as e:\n            logger.error(\"RabbitMQ management API call failed: %s\", e)\n            return []\n        finally:\n            http_client.close()\n\n        if response.code == 200:\n            info = json.loads(response.body.decode())\n            return [x for x in info if x['name'] in names]\n        response.rethrow()\n\n    @classmethod\n    def validate_http_api(cls, http_api):\n        url = urlparse(http_api)\n        if url.scheme not in ('http', 'https'):\n            raise ValueError(f\"Invalid http api schema: {url.scheme}\")\n\n\nclass RedisBase(BrokerBase):\n    DEFAULT_SEP = '\\x06\\x16'\n    DEFAULT_PRIORITY_STEPS = [0, 3, 6, 9]\n\n    def __init__(self, broker_url, *_, **kwargs):\n        super().__init__(broker_url)\n        self.redis = None\n\n        if not redis:\n            raise ImportError('redis library is required')\n\n        broker_options = kwargs.get('broker_options', {})\n        self.priority_steps = broker_options.get(\n            'priority_steps', self.DEFAULT_PRIORITY_STEPS)\n        self.sep = broker_options.get('sep', self.DEFAULT_SEP)\n        self.broker_prefix = broker_options.get('global_keyprefix', '')\n\n    def _q_for_pri(self, queue, pri):\n        if pri not in self.priority_steps:\n            raise ValueError('Priority not in priority steps')\n        # pylint: disable=consider-using-f-string\n        return '{0}{1}{2}'.format(*((queue, self.sep, pri) if pri else (queue, '', '')))\n\n    async def queues(self, names):\n        queue_stats = []\n        for name in names:\n            priority_names = [self.broker_prefix + self._q_for_pri(\n                name, pri) for pri in self.priority_steps]\n            queue_stats.append({\n                'name': name,\n                'messages': sum((self.redis.llen(x) for x in priority_names))\n            })\n        return queue_stats\n\n\nclass Redis(RedisBase):\n\n    def __init__(self, broker_url, *args, **kwargs):\n        super().__init__(broker_url, *args, **kwargs)\n        self.host = self.host or 'localhost'\n        self.port = self.port or 6379\n        self.vhost = self._prepare_virtual_host(self.vhost)\n        self.redis = self._get_redis_client()\n\n    def _prepare_virtual_host(self, vhost):\n        if not isinstance(vhost, numbers.Integral):\n            if not vhost or vhost == '/':\n                vhost = 0\n            elif vhost.startswith('/'):\n                vhost = vhost[1:]\n            try:\n                vhost = int(vhost)\n            except ValueError as exc:\n                raise ValueError(f'Database is int between 0 and limit - 1, not {vhost}') from exc\n        return vhost\n\n    def _get_redis_client_args(self):\n        return {\n            'host': self.host,\n            'port': self.port,\n            'db': self.vhost,\n            'username': self.username,\n            'password': self.password\n        }\n\n    def _get_redis_client(self):\n        return redis.Redis(**self._get_redis_client_args())\n\n\nclass RedisSentinel(RedisBase):\n\n    def __init__(self, broker_url, *args, **kwargs):\n        super().__init__(broker_url, *args, **kwargs)\n        broker_options = kwargs.get('broker_options', {})\n        broker_use_ssl = kwargs.get('broker_use_ssl', None)\n        self.host = self.host or 'localhost'\n        self.port = self.port or 26379\n        self.vhost = self._prepare_virtual_host(self.vhost)\n        self.master_name = self._prepare_master_name(broker_options)\n        self.redis = self._get_redis_client(broker_options, broker_use_ssl)\n\n    def _prepare_virtual_host(self, vhost):\n        if not isinstance(vhost, numbers.Integral):\n            if not vhost or vhost == '/':\n                vhost = 0\n            elif vhost.startswith('/'):\n                vhost = vhost[1:]\n            try:\n                vhost = int(vhost)\n            except ValueError as exc:\n                raise ValueError('Database is int between 0 and limit - 1, not {vhost}') from exc\n        return vhost\n\n    def _prepare_master_name(self, broker_options):\n        try:\n            master_name = broker_options['master_name']\n        except KeyError as exc:\n            raise ValueError('master_name is required for Sentinel broker') from exc\n        return master_name\n\n    def _get_redis_client(self, broker_options, broker_use_ssl):\n        connection_kwargs = {\n            'password': self.password,\n            'sentinel_kwargs': broker_options.get('sentinel_kwargs')\n        }\n        if isinstance(broker_use_ssl, dict):\n            connection_kwargs['ssl'] = True\n            connection_kwargs.update(broker_use_ssl)\n        # get all sentinel hosts from Celery App config and use them to initialize Sentinel\n        sentinel = redis.sentinel.Sentinel(\n            [(self.host, self.port)], **connection_kwargs)\n        redis_client = sentinel.master_for(self.master_name)\n        return redis_client\n\n\nclass RedisSocket(RedisBase):\n\n    def __init__(self, broker_url, *args, **kwargs):\n        super().__init__(broker_url, *args, **kwargs)\n        self.redis = redis.Redis(unix_socket_path='/' + self.vhost,\n                                 password=self.password)\n\n\nclass RedisSsl(Redis):\n    \"\"\"\n    Redis SSL class offering connection to the broker over SSL.\n    This does not currently support SSL settings through the url, only through\n    the broker_use_ssl celery configuration.\n    \"\"\"\n\n    def __init__(self, broker_url, *args, **kwargs):\n        if 'broker_use_ssl' not in kwargs:\n            raise ValueError('rediss broker requires broker_use_ssl')\n        self.broker_use_ssl = kwargs.get('broker_use_ssl', {})\n        super().__init__(broker_url, *args, **kwargs)\n\n    def _get_redis_client_args(self):\n        client_args = super()._get_redis_client_args()\n        client_args['ssl'] = True\n        if isinstance(self.broker_use_ssl, dict):\n            client_args.update(self.broker_use_ssl)\n        return client_args\n\n\nclass Broker:\n    \"\"\"Factory returning the appropriate broker client based on URL scheme.\n\n    Supported schemes:\n    ``amqp`` or ``amqps``  -> :class:`RabbitMQ`\n    ``redis``              -> :class:`Redis`\n    ``rediss``             -> :class:`RedisSsl`\n    ``redis+socket``       -> :class:`RedisSocket`\n    ``sentinel``           -> :class:`RedisSentinel`\n    \"\"\"\n\n    def __new__(cls, broker_url, *args, **kwargs):\n        scheme = urlparse(broker_url).scheme\n        if scheme in ('amqp', 'amqps'):\n            return RabbitMQ(broker_url, *args, **kwargs)\n        if scheme == 'redis':\n            return Redis(broker_url, *args, **kwargs)\n        if scheme == 'rediss':\n            return RedisSsl(broker_url, *args, **kwargs)\n        if scheme == 'redis+socket':\n            return RedisSocket(broker_url, *args, **kwargs)\n        if scheme == 'sentinel':\n            return RedisSentinel(broker_url, *args, **kwargs)\n        raise NotImplementedError\n\n    async def queues(self, names):\n        raise NotImplementedError\n\n\nasync def main():\n    broker_url = sys.argv[1] if len(sys.argv) > 1 else 'amqp://'\n    queue_name = sys.argv[2] if len(sys.argv) > 2 else 'celery'\n    if len(sys.argv) > 3:\n        http_api = sys.argv[3]\n    else:\n        http_api = 'http://guest:guest@localhost:15672/api/'\n\n    broker = Broker(broker_url, http_api=http_api)\n    queues = await broker.queues([queue_name])\n    if queues:\n        print(queues)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "flower/utils/search.py",
    "content": "import re\n\nfrom kombu.utils.encoding import safe_str\n\n\ndef parse_search_terms(raw_search_value):\n    search_regexp = r'(?:[^\\s,\"]|\"(?:\\\\.|[^\"])*\")+'  # splits by space, ignores space in quotes\n    if not raw_search_value:\n        return {}\n    parsed_search = {}\n    for query_part in re.findall(search_regexp, raw_search_value):\n        if not query_part:\n            continue\n        if query_part.startswith('result:'):\n            parsed_search['result'] = preprocess_search_value(query_part[len('result:'):])\n        elif query_part.startswith('args:'):\n            if 'args' not in parsed_search:\n                parsed_search['args'] = []\n            parsed_search['args'].append(preprocess_search_value(query_part[len('args:'):]))\n        elif query_part.startswith('kwargs:'):\n            if 'kwargs'not in parsed_search:\n                parsed_search['kwargs'] = {}\n            try:\n                key, value = [p.strip() for p in query_part[len('kwargs:'):].split('=')]\n            except ValueError:\n                continue\n            parsed_search['kwargs'][key] = preprocess_search_value(value)\n        elif query_part.startswith('state'):\n            if 'state' not in parsed_search:\n                parsed_search['state'] = []\n            parsed_search['state'].append(preprocess_search_value(query_part[len('state:'):]))\n        else:\n            parsed_search['any'] = preprocess_search_value(query_part)\n    return parsed_search\n\n\ndef satisfies_search_terms(task, search_terms):\n    any_value_search_term = search_terms.get('any')\n    result_search_term = search_terms.get('result')\n    args_search_terms = search_terms.get('args')\n    kwargs_search_terms = search_terms.get('kwargs')\n    state_search_terms = search_terms.get('state')\n\n    if not any([any_value_search_term, result_search_term, args_search_terms, kwargs_search_terms, state_search_terms]):\n        return True\n\n    terms = [\n        state_search_terms and task.state in state_search_terms,\n        any_value_search_term and any_value_search_term in '|'.join(\n            filter(None, [task.name, task.uuid, task.state,\n                          task.worker.hostname if task.worker else None,\n                          task.args, task.kwargs, safe_str(task.result)])),\n        result_search_term and task.result and result_search_term in task.result,\n        kwargs_search_terms and all(\n            stringified_dict_contains_value(k, v, task.kwargs) for k, v in kwargs_search_terms.items()\n        ),\n        args_search_terms and task_args_contains_search_args(task.args, args_search_terms)\n    ]\n    return any(terms)\n\n\ndef stringified_dict_contains_value(key, value, str_dict):\n    \"\"\"Checks if dict in for of string like \"{'test': 5}\" contains\n    key/value pair. This works faster, then creating actual dict\n    from string since this operation is called for each task in case\n    of kwargs search.\"\"\"\n    if not str_dict:\n        return False\n    value = str(value)\n    try:\n        # + 3 for key right quote, one for colon and one for space\n        key_index = str_dict.index(key) + len(key) + 3\n    except ValueError:\n        return False\n    try:\n        comma_index = str_dict.index(',', key_index)\n    except ValueError:\n        # last value in dict\n        comma_index = str_dict.index('}', key_index)\n    return str(value) == str_dict[key_index:comma_index].strip('\"\\'')\n\n\ndef preprocess_search_value(raw_value):\n    return raw_value.strip('\" ') if raw_value else ''\n\n\ndef task_args_contains_search_args(task_args, search_args):\n    if not task_args:\n        return False\n    return all(a in task_args for a in search_args)\n"
  },
  {
    "path": "flower/utils/tasks.py",
    "content": "import datetime\nimport time\n\nfrom .search import parse_search_terms, satisfies_search_terms\n\n\n# pylint: disable=too-many-branches,too-many-locals,too-many-arguments\ndef iter_tasks(events, limit=None, offset=0, type=None, worker=None, state=None,\n               sort_by=None, received_start=None, received_end=None,\n               started_start=None, started_end=None, search=None):\n    i = 0\n    tasks = events.state.tasks_by_timestamp()\n    if sort_by is not None:\n        tasks = sort_tasks(tasks, sort_by)\n\n    def convert(x):\n        return time.mktime(datetime.datetime.strptime(x, '%Y-%m-%d %H:%M').timetuple())\n\n    search_terms = parse_search_terms(search or {})\n\n    for uuid, task in tasks:\n        if type and task.name != type:\n            continue\n        if worker and task.worker and task.worker.hostname != worker:\n            continue\n        if state and task.state != state:\n            continue\n        if received_start and task.received and\\\n                task.received < convert(received_start):\n            continue\n        if received_end and task.received and\\\n                task.received > convert(received_end):\n            continue\n        if started_start and task.started and\\\n                task.started < convert(started_start):\n            continue\n        if started_end and task.started and\\\n                task.started > convert(started_end):\n            continue\n        if not satisfies_search_terms(task, search_terms):\n            continue\n        if i >= offset:\n            yield uuid, task\n        i += 1\n        if limit is not None:\n            if i == limit + offset:\n                break\n\n\nsort_keys = {'name': str, 'state': str, 'received': float, 'started': float}\n\n\ndef sort_tasks(tasks, sort_by):\n    assert sort_by.lstrip('-') in sort_keys\n    reverse = False\n    if sort_by.startswith('-'):\n        sort_by = sort_by.lstrip('-')\n        reverse = True\n    yield from sorted(\n            tasks,\n            key=lambda x: getattr(x[1], sort_by) or sort_keys[sort_by](),\n            reverse=reverse)\n\n\ndef get_task_by_id(events, task_id):\n    return events.state.tasks.get(task_id)\n\n\ndef as_dict(task):\n    return task.as_dict()\n"
  },
  {
    "path": "flower/utils/template.py",
    "content": "import re\nfrom datetime import datetime, timedelta\n\nfrom celery import current_app\nfrom humanize import naturaltime\nfrom pytz import timezone, utc\n\nKEYWORDS_UP = ('ssl', 'uri', 'url', 'uuid', 'eta')\nKEYWORDS_DOWN = ('args', 'kwargs')\nUUID_REGEX = re.compile(r'^[\\w]{8}(-[\\w]{4}){3}-[\\w]{12}$')\n\n\ndef format_time(time, tz):\n    dt = datetime.fromtimestamp(time, tz=tz)\n    return dt.strftime(\"%Y-%m-%d %H:%M:%S.%f %Z\")\n\n\ndef humanize(obj, type=None, length=None):\n    if obj is None:\n        obj = ''\n    elif type and type.startswith('time'):\n        tz = type[len('time'):].lstrip('-')\n        tz = timezone(tz) if tz else getattr(current_app, 'timezone', '') or utc\n        obj = format_time(float(obj), tz) if obj else ''\n    elif type and type.startswith('natural-time'):\n        tz = type[len('natural-time'):].lstrip('-')\n        tz = timezone(tz) if tz else getattr(current_app, 'timezone', '') or utc\n        delta = datetime.now(tz) - datetime.fromtimestamp(float(obj), tz)\n        if delta < timedelta(days=1):\n            obj = naturaltime(delta)\n        else:\n            obj = format_time(float(obj), tz) if obj else ''\n    elif isinstance(obj, str) and not re.match(UUID_REGEX, obj):\n        obj = obj.replace('-', ' ').replace('_', ' ')\n        obj = re.sub('|'.join(KEYWORDS_UP),\n                     lambda m: m.group(0).upper(), obj)\n        if obj and obj not in KEYWORDS_DOWN:\n            obj = obj[0].upper() + obj[1:]\n    elif isinstance(obj, list):\n        if all(isinstance(x, (int, float, str)) for x in obj):\n            obj = ', '.join(map(str, obj))\n    if length is not None and len(obj) > length:\n        obj = obj[:length - 4] + ' ...'\n    return obj\n"
  },
  {
    "path": "flower/views/__init__.py",
    "content": "import re\nimport inspect\nimport traceback\nimport copy\nimport logging\nimport hmac\n\nfrom base64 import b64decode\n\nimport tornado\n\nfrom ..utils import template, bugreport, strtobool\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseHandler(tornado.web.RequestHandler):\n    def set_default_headers(self):\n        if not (self.application.options.basic_auth or self.application.options.auth):\n            self.set_header(\"Access-Control-Allow-Origin\", \"*\")\n            self.set_header(\"Access-Control-Allow-Headers\",\n                            \"x-requested-with,access-control-allow-origin,authorization,content-type\")\n            self.set_header('Access-Control-Allow-Methods',\n                            ' PUT, DELETE, OPTIONS, POST, GET, PATCH')\n\n    def options(self, *_, **__):\n        self.set_status(204)\n        self.finish()\n\n    def render(self, *args, **kwargs):\n        app_options = self.application.options\n        functions = inspect.getmembers(template, inspect.isfunction)\n        assert not set(map(lambda x: x[0], functions)) & set(kwargs.keys())\n        kwargs.update(functions)\n        kwargs.update(url_prefix=app_options.url_prefix)\n        super().render(*args, **kwargs)\n\n    def write_error(self, status_code, **kwargs):\n        if status_code in (404, 403):\n            message = ''\n            if 'exc_info' in kwargs and kwargs['exc_info'][0] == tornado.web.HTTPError:\n                message = kwargs['exc_info'][1].log_message\n            self.render('404.html', message=message)\n        elif status_code == 500:\n            error_trace = \"\".join(traceback.format_exception(*kwargs['exc_info']))\n\n            self.render('error.html',\n                    debug=self.application.options.debug,\n                    status_code=status_code,\n                    error_trace=error_trace,\n                    bugreport=bugreport())\n        elif status_code == 401:\n            self.set_status(status_code)\n            self.set_header('WWW-Authenticate', 'Basic realm=\"flower\"')\n            self.finish('Access denied')\n        else:\n            message = ''\n            if 'exc_info' in kwargs and kwargs['exc_info'][0] == tornado.web.HTTPError:\n                message = kwargs['exc_info'][1].log_message\n                self.set_header('Content-Type', 'text/plain')\n                self.write(str(message))\n            self.set_status(status_code)\n            self.finish()\n\n    def get_current_user(self):\n        # Basic Auth\n        basic_auth = self.application.options.basic_auth\n        if basic_auth:\n            auth_header = self.request.headers.get(\"Authorization\", \"\")\n            try:\n                basic, credentials = auth_header.split()\n                credentials = b64decode(credentials.encode()).decode()\n                if basic != 'Basic':\n                    raise tornado.web.HTTPError(401)\n                for stored_credential in basic_auth:\n                    if hmac.compare_digest(stored_credential, credentials):\n                        break\n                else:\n                    raise tornado.web.HTTPError(401)\n            except ValueError as exc:\n                raise tornado.web.HTTPError(401) from exc\n\n        # OAuth2\n        if not self.application.options.auth:\n            return True\n        user = self.get_secure_cookie('user')\n        if user:\n            if not isinstance(user, str):\n                user = user.decode()\n            if re.match(self.application.options.auth, user):\n                return user\n        return None\n\n    # pylint: disable=dangerous-default-value\n    def get_argument(self, name, default=[], strip=True, type=None):\n        arg = super().get_argument(name, default, strip)\n        if arg and isinstance(arg, str):\n            arg = tornado.escape.xhtml_escape(arg)\n        if type is not None:\n            try:\n                if type is bool:\n                    arg = strtobool(str(arg))\n                else:\n                    arg = type(arg)\n            except (ValueError, TypeError) as exc:\n                if arg is None and default is None:\n                    return arg\n                raise tornado.web.HTTPError(\n                        400,\n                        f\"Invalid argument '{arg}' of type '{type.__name__}'\") from exc\n        return arg\n\n    @property\n    def capp(self):\n        \"return Celery application object\"\n        return self.application.capp\n\n    def format_task(self, task):\n        custom_format_task = self.application.options.format_task\n        if custom_format_task:\n            try:\n                task = custom_format_task(copy.copy(task))\n            except Exception:\n                logger.exception(\"Failed to format '%s' task\", task.uuid)\n        return task\n\n    def get_active_queue_names(self):\n        queues = set([])\n        for _, info in self.application.workers.items():\n            for queue in info.get('active_queues', []):\n                queues.add(queue['name'])\n\n        if not queues:\n            queues = set([self.capp.conf.task_default_queue]) |\\\n                {q.name for q in self.capp.conf.task_queues or [] if q.name}\n        return sorted(queues)\n"
  },
  {
    "path": "flower/views/auth.py",
    "content": "import json\nimport os\nimport re\nimport uuid\nfrom urllib.parse import urlencode\n\nimport tornado.auth\nimport tornado.gen\nimport tornado.web\nfrom celery.utils.imports import instantiate\nfrom tornado.options import options\n\nfrom ..views import BaseHandler\nfrom ..views.error import NotFoundErrorHandler\n\n# pylint: disable=invalid-name\n\n\ndef authenticate(pattern, email):\n    if '|' in pattern:\n        return email in pattern.split('|')\n    if '*' in pattern:\n        pattern = re.escape(pattern).replace(r'\\.\\*', r\"[A-Za-z0-9!#$%&'*+/=?^_`{|}~.\\-]*\")\n        return re.fullmatch(pattern, email)\n    return pattern == email\n\n\ndef validate_auth_option(pattern):\n    if pattern.count('*') > 1:\n        return False\n    if '*' in pattern and '|' in pattern:\n        return False\n    if '*' in pattern.rsplit('@', 1)[-1]:\n        return False\n    return True\n\n\nclass GoogleAuth2LoginHandler(BaseHandler, tornado.auth.GoogleOAuth2Mixin):\n    _OAUTH_SETTINGS_KEY = 'oauth'\n\n    async def get(self):\n        redirect_uri = self.settings[self._OAUTH_SETTINGS_KEY]['redirect_uri']\n        if self.get_argument('code', False):\n            user = await self.get_authenticated_user(\n                redirect_uri=redirect_uri,\n                code=self.get_argument('code'),\n            )\n            await self._on_auth(user)\n        else:\n            self.authorize_redirect(\n                redirect_uri=redirect_uri,\n                client_id=self.settings[self._OAUTH_SETTINGS_KEY]['key'],\n                scope=['profile', 'email'],\n                response_type='code',\n                extra_params={'approval_prompt': ''}\n            )\n\n    async def _on_auth(self, user):\n        if not user:\n            raise tornado.web.HTTPError(403, 'Google auth failed')\n        access_token = user['access_token']\n\n        try:\n            response = await self.get_auth_http_client().fetch(\n                'https://www.googleapis.com/userinfo/v2/me',\n                headers={'Authorization': f'Bearer {access_token}'})\n        except Exception as e:\n            raise tornado.web.HTTPError(403, f'Google auth failed: {e}')\n\n        email = json.loads(response.body.decode('utf-8'))['email']\n        if not authenticate(self.application.options.auth, email):\n            message = f\"Access denied to '{email}'. Please use another account or ask your admin to add your email to flower --auth.\"\n            raise tornado.web.HTTPError(403, message)\n\n        self.set_secure_cookie(\"user\", str(email))\n\n        next_ = self.get_argument('next', self.application.options.url_prefix or '/')\n        if self.application.options.url_prefix and next_[0] != '/':\n            next_ = '/' + next_\n\n        self.redirect(next_)\n\n\nclass LoginHandler(BaseHandler):\n    def __new__(cls, *args, **kwargs):\n        return instantiate(options.auth_provider or NotFoundErrorHandler, *args, **kwargs)\n\n\nclass GithubLoginHandler(BaseHandler, tornado.auth.OAuth2Mixin):\n\n    _OAUTH_DOMAIN = os.getenv(\n        \"FLOWER_GITHUB_OAUTH_DOMAIN\", \"github.com\")\n    _OAUTH_AUTHORIZE_URL = f'https://{_OAUTH_DOMAIN}/login/oauth/authorize'\n    _OAUTH_ACCESS_TOKEN_URL = f'https://{_OAUTH_DOMAIN}/login/oauth/access_token'\n    _OAUTH_NO_CALLBACKS = False\n    _OAUTH_SETTINGS_KEY = 'oauth'\n\n    async def get_authenticated_user(self, redirect_uri, code):\n        body = urlencode({\n            \"redirect_uri\": redirect_uri,\n            \"code\": code,\n            \"client_id\": self.settings[self._OAUTH_SETTINGS_KEY]['key'],\n            \"client_secret\": self.settings[self._OAUTH_SETTINGS_KEY]['secret'],\n            \"grant_type\": \"authorization_code\",\n        })\n\n        response = await self.get_auth_http_client().fetch(\n            self._OAUTH_ACCESS_TOKEN_URL,\n            method=\"POST\",\n            headers={'Content-Type': 'application/x-www-form-urlencoded',\n                     'Accept': 'application/json'}, body=body)\n\n        if response.error:\n            raise tornado.auth.AuthError(f'OAuth authenticator error: {response}')\n\n        return json.loads(response.body.decode('utf-8'))\n\n    async def get(self):\n        redirect_uri = self.settings[self._OAUTH_SETTINGS_KEY]['redirect_uri']\n        if self.get_argument('code', False):\n            user = await self.get_authenticated_user(\n                redirect_uri=redirect_uri,\n                code=self.get_argument('code'),\n            )\n            await self._on_auth(user)\n        else:\n            self.authorize_redirect(\n                redirect_uri=redirect_uri,\n                client_id=self.settings[self._OAUTH_SETTINGS_KEY]['key'],\n                scope=['user:email'],\n                response_type='code',\n                extra_params={'approval_prompt': ''}\n            )\n\n    async def _on_auth(self, user):\n        if not user:\n            raise tornado.web.HTTPError(500, 'OAuth authentication failed')\n        access_token = user['access_token']\n\n        response = await self.get_auth_http_client().fetch(\n            f'https://api.{self._OAUTH_DOMAIN}/user/emails',\n            headers={'Authorization': 'token ' + access_token,\n                     'User-agent': 'Tornado auth'})\n\n        emails = [email['email'].lower() for email in json.loads(response.body.decode('utf-8'))\n                  if email['verified'] and authenticate(self.application.options.auth, email['email'])]\n\n        if not emails:\n            message = (\n                \"Access denied. Please use another account or \"\n                \"ask your admin to add your email to flower --auth.\"\n            )\n            raise tornado.web.HTTPError(403, message)\n\n        self.set_secure_cookie(\"user\", str(emails.pop()))\n\n        next_ = self.get_argument('next', self.application.options.url_prefix or '/')\n        if self.application.options.url_prefix and next_[0] != '/':\n            next_ = '/' + next_\n        self.redirect(next_)\n\n\nclass GitLabLoginHandler(BaseHandler, tornado.auth.OAuth2Mixin):\n\n    _OAUTH_GITLAB_DOMAIN = os.getenv(\n        \"FLOWER_GITLAB_OAUTH_DOMAIN\", \"gitlab.com\")\n    _OAUTH_AUTHORIZE_URL = f'https://{_OAUTH_GITLAB_DOMAIN}/oauth/authorize'\n    _OAUTH_ACCESS_TOKEN_URL = f'https://{_OAUTH_GITLAB_DOMAIN}/oauth/token'\n    _OAUTH_NO_CALLBACKS = False\n\n    async def get_authenticated_user(self, redirect_uri, code):\n        body = urlencode({\n            'redirect_uri': redirect_uri,\n            'code': code,\n            'client_id': self.settings['oauth']['key'],\n            'client_secret': self.settings['oauth']['secret'],\n            'grant_type': 'authorization_code',\n        })\n        response = await self.get_auth_http_client().fetch(\n            self._OAUTH_ACCESS_TOKEN_URL,\n            method='POST',\n            headers={'Content-Type': 'application/x-www-form-urlencoded',\n                     'Accept': 'application/json'},\n            body=body\n        )\n        if response.error:\n            raise tornado.auth.AuthError(f'OAuth authenticator error: {response}')\n        return json.loads(response.body.decode('utf-8'))\n\n    async def get(self):\n        redirect_uri = self.settings['oauth']['redirect_uri']\n        if self.get_argument('code', False):\n            user = await self.get_authenticated_user(\n                redirect_uri=redirect_uri,\n                code=self.get_argument('code'),\n            )\n            await self._on_auth(user)\n        else:\n            self.authorize_redirect(\n                redirect_uri=redirect_uri,\n                client_id=self.settings['oauth']['key'],\n                scope=['read_api'],\n                response_type='code',\n                extra_params={'approval_prompt': ''},\n            )\n\n    async def _on_auth(self, user):\n        if not user:\n            raise tornado.web.HTTPError(500, 'OAuth authentication failed')\n        access_token = user['access_token']\n        allowed_groups = os.environ.get('FLOWER_GITLAB_AUTH_ALLOWED_GROUPS', '')\n        allowed_groups = [group.strip() for group in allowed_groups.split(',') if group]\n\n        # Check user email address against regexp\n        try:\n            response = await self.get_auth_http_client().fetch(\n                f'https://{self._OAUTH_GITLAB_DOMAIN}/api/v4/user',\n                headers={'Authorization': 'Bearer ' + access_token,\n                         'User-agent': 'Tornado auth'}\n            )\n        except Exception as e:\n            raise tornado.web.HTTPError(403, f'GitLab auth failed: {e}')\n\n        user_email = json.loads(response.body.decode('utf-8'))['email']\n        email_allowed = authenticate(self.application.options.auth, user_email)\n\n        # Check user's groups against list of allowed groups\n        matching_groups = []\n        if allowed_groups:\n            min_access_level = os.environ.get('FLOWER_GITLAB_MIN_ACCESS_LEVEL', '20')\n            response = await self.get_auth_http_client().fetch(\n                f'https://{self._OAUTH_GITLAB_DOMAIN}/api/v4/groups?min_access_level={min_access_level}',\n                headers={\n                    'Authorization': 'Bearer ' + access_token,\n                    'User-agent': 'Tornado auth'\n                }\n            )\n            matching_groups = [\n                group['id']\n                for group in json.loads(response.body.decode('utf-8'))\n                if group['full_path'] in allowed_groups\n            ]\n\n        if not email_allowed or (allowed_groups and len(matching_groups) == 0):\n            message = 'Access denied. Please use another account or contact your admin.'\n            raise tornado.web.HTTPError(403, message)\n\n        self.set_secure_cookie('user', str(user_email))\n        next_ = self.get_argument('next', self.application.options.url_prefix or '/')\n        if self.application.options.url_prefix and next_[0] != '/':\n            next_ = '/' + next_\n        self.redirect(next_)\n\n\nclass OktaLoginHandler(BaseHandler, tornado.auth.OAuth2Mixin):\n    _OAUTH_NO_CALLBACKS = False\n    _OAUTH_SETTINGS_KEY = 'oauth'\n\n    @property\n    def base_url(self):\n        return os.environ.get('FLOWER_OAUTH2_OKTA_BASE_URL')\n\n    @property\n    def _OAUTH_AUTHORIZE_URL(self):\n        return f\"{self.base_url}/v1/authorize\"\n\n    @property\n    def _OAUTH_ACCESS_TOKEN_URL(self):\n        return f\"{self.base_url}/v1/token\"\n\n    @property\n    def _OAUTH_USER_INFO_URL(self):\n        return f\"{self.base_url}/v1/userinfo\"\n\n    async def get_access_token(self, redirect_uri, code):\n        body = urlencode({\n            \"redirect_uri\": redirect_uri,\n            \"code\": code,\n            \"client_id\": self.settings[self._OAUTH_SETTINGS_KEY]['key'],\n            \"client_secret\": self.settings[self._OAUTH_SETTINGS_KEY]['secret'],\n            \"grant_type\": \"authorization_code\",\n        })\n\n        response = await self.get_auth_http_client().fetch(\n            self._OAUTH_ACCESS_TOKEN_URL,\n            method=\"POST\",\n            headers={'Content-Type': 'application/x-www-form-urlencoded',\n                     'Accept': 'application/json'}, body=body)\n\n        if response.error:\n            raise tornado.auth.AuthError(f'OAuth authenticator error: {response}')\n\n        return json.loads(response.body.decode('utf-8'))\n\n    async def get(self):\n        redirect_uri = self.settings[self._OAUTH_SETTINGS_KEY]['redirect_uri']\n        if self.get_argument('code', False):\n            expected_state = (self.get_secure_cookie('oauth_state') or b'').decode('utf-8')\n            returned_state = self.get_argument('state')\n\n            if returned_state is None or returned_state != expected_state:\n                raise tornado.auth.AuthError(\n                    'OAuth authenticator error: State tokens do not match')\n\n            access_token_response = await self.get_access_token(\n                redirect_uri=redirect_uri,\n                code=self.get_argument('code'),\n            )\n            await self._on_auth(access_token_response)\n        else:\n            state = str(uuid.uuid4())\n            self.set_secure_cookie(\"oauth_state\", state)\n            self.authorize_redirect(\n                redirect_uri=redirect_uri,\n                client_id=self.settings[self._OAUTH_SETTINGS_KEY]['key'],\n                scope=['openid email'],\n                response_type='code',\n                extra_params={'state': state}\n            )\n\n    async def _on_auth(self, access_token_response):\n        if not access_token_response:\n            raise tornado.web.HTTPError(500, 'OAuth authentication failed')\n        access_token = access_token_response['access_token']\n\n        response = await self.get_auth_http_client().fetch(\n            self._OAUTH_USER_INFO_URL,\n            headers={'Authorization': 'Bearer ' + access_token,\n                     'User-agent': 'Tornado auth'})\n\n        decoded_body = json.loads(response.body.decode('utf-8'))\n        email = (decoded_body.get('email') or '').strip()\n        email_verified = (\n            decoded_body.get('email_verified') and\n            authenticate(self.application.options.auth, email)\n        )\n\n        if not email_verified:\n            message = (\n                \"Access denied. Please use another account or \"\n                \"ask your admin to add your email to flower --auth.\"\n            )\n            raise tornado.web.HTTPError(403, message)\n\n        self.set_secure_cookie(\"user\", str(email))\n        self.clear_cookie('oauth_state')\n\n        next_ = self.get_argument('next', self.application.options.url_prefix or '/')\n        if self.application.options.url_prefix and next_[0] != '/':\n            next_ = '/' + next_\n        self.redirect(next_)\n"
  },
  {
    "path": "flower/views/broker.py",
    "content": "import logging\n\nfrom tornado import web\n\nfrom ..utils.broker import Broker\nfrom ..views import BaseHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass BrokerView(BaseHandler):\n    @web.authenticated\n    async def get(self):\n        app = self.application\n\n        http_api = None\n        if app.transport == 'amqp' and app.options.broker_api:\n            http_api = app.options.broker_api\n\n        try:\n            broker = Broker(app.capp.connection(connect_timeout=1.0).as_uri(include_password=True),\n                            http_api=http_api, broker_options=self.capp.conf.broker_transport_options,\n                            broker_use_ssl=self.capp.conf.broker_use_ssl)\n        except NotImplementedError as exc:\n            raise web.HTTPError(\n                404, f\"'{app.transport}' broker is not supported\") from exc\n\n        try:\n            queues = await broker.queues(self.get_active_queue_names())\n        except Exception as e:\n            queues = []\n            logger.error(\"Unable to get queues: '%s'\", e)\n\n        self.render(\"broker.html\",\n                    broker_url=app.capp.connection().as_uri(),\n                    queues=queues)\n"
  },
  {
    "path": "flower/views/error.py",
    "content": "import tornado.web\n\nfrom ..views import BaseHandler\n\n\nclass NotFoundErrorHandler(BaseHandler):\n    def get(self):\n        raise tornado.web.HTTPError(404)\n\n    def post(self):\n        raise tornado.web.HTTPError(404)\n"
  },
  {
    "path": "flower/views/monitor.py",
    "content": "import prometheus_client\n\nfrom ..views import BaseHandler\n\n\nclass Metrics(BaseHandler):\n    async def get(self):\n        self.write(prometheus_client.generate_latest())\n        self.set_header(\"Content-Type\", \"text/plain\")\n\n\nclass Healthcheck(BaseHandler):\n    async def get(self):\n        self.write(\"OK\")\n"
  },
  {
    "path": "flower/views/tasks.py",
    "content": "import copy\nimport logging\nfrom functools import total_ordering\n\nfrom tornado import web\n\nfrom ..utils.tasks import as_dict, get_task_by_id, iter_tasks\nfrom ..views import BaseHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass TaskView(BaseHandler):\n    @web.authenticated\n    def get(self, task_id):\n        task = get_task_by_id(self.application.events, task_id)\n\n        if task is None:\n            raise web.HTTPError(404, f\"Unknown task '{task_id}'\")\n        task = self.format_task(task)\n        self.render(\"task.html\", task=task)\n\n\n@total_ordering\nclass Comparable:\n    \"\"\"\n    Compare two objects, one or more of which may be None.  If one of the\n    values is None, the other will be deemed greater.\n    \"\"\"\n\n    def __init__(self, value):\n        self.value = value\n\n    def __eq__(self, other):\n        return self.value == other.value\n\n    def __lt__(self, other):\n        try:\n            return self.value < other.value\n        except TypeError:\n            return self.value is None\n\n\nclass TasksDataTable(BaseHandler):\n    @web.authenticated\n    def get(self):\n        app = self.application\n        draw = self.get_argument('draw', type=int)\n        start = self.get_argument('start', type=int)\n        length = self.get_argument('length', type=int)\n        search = self.get_argument('search[value]', type=str)\n\n        column = self.get_argument('order[0][column]', type=int)\n        sort_by = self.get_argument(f'columns[{column}][data]', type=str)\n        sort_order = self.get_argument('order[0][dir]', type=str) == 'desc'\n\n        def key(item):\n            return Comparable(getattr(item[1], sort_by))\n\n        self.maybe_normalize_for_sort(app.events.state.tasks_by_timestamp(), sort_by)\n\n        sorted_tasks = sorted(\n            iter_tasks(app.events, search=search),\n            key=key,\n            reverse=sort_order\n        )\n\n        filtered_tasks = []\n\n        for task in sorted_tasks[start:start + length]:\n            task_dict = as_dict(self.format_task(task)[1])\n            if task_dict.get('worker'):\n                task_dict['worker'] = task_dict['worker'].hostname\n\n            filtered_tasks.append(task_dict)\n\n        self.write(dict(draw=draw, data=filtered_tasks,\n                        recordsTotal=len(sorted_tasks),\n                        recordsFiltered=len(sorted_tasks)))\n\n    @classmethod\n    def maybe_normalize_for_sort(cls, tasks, sort_by):\n        sort_keys = {'name': str, 'state': str, 'received': float, 'started': float, 'runtime': float}\n        if sort_by in sort_keys:\n            for _, task in tasks:\n                attr_value = getattr(task, sort_by, None)\n                if attr_value:\n                    try:\n                        setattr(task, sort_by, sort_keys[sort_by](attr_value))\n                    except TypeError:\n                        pass\n\n    @web.authenticated\n    def post(self):\n        return self.get()\n\n    def format_task(self, task):\n        uuid, args = task\n        custom_format_task = self.application.options.format_task\n\n        if custom_format_task:\n            try:\n                args = custom_format_task(copy.copy(args))\n            except Exception:\n                logger.exception(\"Failed to format '%s' task\", uuid)\n        return uuid, args\n\n\nclass TasksView(BaseHandler):\n    @web.authenticated\n    def get(self):\n        app = self.application\n        capp = self.application.capp\n\n        time = 'natural-time' if app.options.natural_time else 'time'\n        if capp.conf.timezone:\n            time += '-' + str(capp.conf.timezone)\n\n        self.render(\n            \"tasks.html\",\n            tasks=[],\n            columns=app.options.tasks_columns,\n            time=time,\n        )\n"
  },
  {
    "path": "flower/views/workers.py",
    "content": "import logging\nimport time\n\nfrom tornado import web\n\nfrom ..options import options\nfrom ..views import BaseHandler\n\nlogger = logging.getLogger(__name__)\n\n\nclass WorkerView(BaseHandler):\n    @web.authenticated\n    async def get(self, name):\n        try:\n            self.application.update_workers(workername=name)\n        except Exception as e:\n            logger.error(e)\n\n        worker = self.application.workers.get(name)\n\n        if worker is None:\n            raise web.HTTPError(404, f\"Unknown worker '{name}'\")\n        if 'stats' not in worker:\n            raise web.HTTPError(404, f\"Unable to get stats for '{name}' worker\")\n\n        self.render(\"worker.html\", worker=dict(worker, name=name))\n\n\nclass WorkersView(BaseHandler):\n    @web.authenticated\n    async def get(self):\n        refresh = self.get_argument('refresh', default=False, type=bool)\n        json = self.get_argument('json', default=False, type=bool)\n\n        events = self.application.events.state\n\n        if refresh:\n            try:\n                self.application.update_workers()\n            except Exception as e:\n                logger.exception('Failed to update workers: %s', e)\n\n        workers = {}\n        for name, values in events.counter.items():\n            if name not in events.workers:\n                continue\n            worker = events.workers[name]\n            info = dict(values)\n            info.update(self._as_dict(worker))\n            info.update(status=worker.alive)\n            workers[name] = info\n\n        if options.purge_offline_workers is not None:\n            timestamp = int(time.time())\n            offline_workers = []\n            for name, info in workers.items():\n                if info.get('status', True):\n                    continue\n\n                heartbeats = info.get('heartbeats', [])\n                last_heartbeat = int(max(heartbeats)) if heartbeats else None\n                if not last_heartbeat or timestamp - last_heartbeat > options.purge_offline_workers:\n                    offline_workers.append(name)\n\n            for name in offline_workers:\n                workers.pop(name)\n\n        if json:\n            self.write(dict(data=list(workers.values())))\n        else:\n            self.render(\"workers.html\",\n                        workers=workers,\n                        broker=self.application.capp.connection().as_uri(),\n                        autorefresh=1 if self.application.options.auto_refresh else 0)\n\n    @classmethod\n    def _as_dict(cls, worker):\n        if hasattr(worker, '_fields'):\n            return dict((k, getattr(worker, k)) for k in worker._fields)\n        return cls._info(worker)\n\n    @classmethod\n    def _info(cls, worker):\n        _fields = ('hostname', 'pid', 'freq', 'heartbeats', 'clock',\n                   'active', 'processed', 'loadavg', 'sw_ident',\n                   'sw_ver', 'sw_sys')\n\n        def _keys():\n            for key in _fields:\n                value = getattr(worker, key, None)\n                if value is not None:\n                    yield key, value\n\n        return dict(_keys())\n"
  },
  {
    "path": "prometheus.yml",
    "content": "global:\n  scrape_interval:     15s\n  evaluation_interval: 15s\n\nscrape_configs:\n  - job_name: flower\n    static_configs:\n      - targets: ['flower:5555']"
  },
  {
    "path": "requirements/default.txt",
    "content": "celery>=5.0.5\ntornado>=5.0.0,<7.0.0\nprometheus_client>=0.8.0\nhumanize\npytz\n"
  },
  {
    "path": "requirements/dev.txt",
    "content": "-r default.txt\n-r test.txt\nredis>=4.3.6\npylint\n"
  },
  {
    "path": "requirements/docs.txt",
    "content": "-r default.txt\nSphinx\nsphinxcontrib-fulltoc\nsphinxcontrib-httpdomain\nsphinxcontrib-redoc\n"
  },
  {
    "path": "requirements/test.txt",
    "content": "\n"
  },
  {
    "path": "scss/build.sh",
    "content": "set -euxo pipefail\n\nBOOTSTRAP_VERSION=\"${1:-5.2.3}\"\nBOOTSTRAP_ZIP=\"v$BOOTSTRAP_VERSION.zip\" \nBOOTSTRAP_DIR=\"bootstrap-$BOOTSTRAP_VERSION\"\n\ncd \"$(git rev-parse --show-toplevel)\"\n\nif [ -f $BOOTSTRAP_ZIP ]; then\n  rm $BOOTSTRAP_ZIP\nfi\n\nif [ ! -d $BOOTSTRAP_DIR ]; then\n\twget https://github.com/twbs/bootstrap/archive/refs/tags/$BOOTSTRAP_ZIP\n\tunzip $BOOTSTRAP_ZIP\n\trm $BOOTSTRAP_ZIP\nfi\n\ncp ./scss/flower.scss $BOOTSTRAP_DIR/scss/flower.scss\n(cd $BOOTSTRAP_DIR && npm install &&\n sass scss/flower.scss dist/css/bootstrap.min.css --style=compressed && npm run js)\n\ncp $BOOTSTRAP_DIR/dist/css/bootstrap.min.css ./flower/static/css/\ncp $BOOTSTRAP_DIR/dist/css/bootstrap.min.css.map ./flower/static/css/\ncp $BOOTSTRAP_DIR/dist/js/bootstrap.bundle.min.js ./flower/static/js/\ncp $BOOTSTRAP_DIR/dist/js/bootstrap.bundle.min.js.map ./flower/static/js/"
  },
  {
    "path": "scss/flower.scss",
    "content": "// flower.scss\n\n$primary: #348613;\n$link-color: #348613;\n$border-color: #c7ecb8;\n$table-striped-bg: #f0ffeb;\n$bg-light: #f0ffeb;\n\n@import \"bootstrap\";"
  },
  {
    "path": "setup.cfg",
    "content": "[wheel]\nuniversal=1\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\nimport os\nimport re\n\nfrom setuptools import setup, find_packages\n\n\nversion = re.compile(r'VERSION\\s*=\\s*\\((.*?)\\)')\n\n\ndef get_package_version():\n    \"returns package version without importing it\"\n    base = os.path.abspath(os.path.dirname(__file__))\n    with open(os.path.join(base, \"flower/__init__.py\")) as initf:\n        for line in initf:\n            m = version.match(line.strip())\n            if not m:\n                continue\n            return \".\".join(m.groups()[0].split(\", \"))\n\n\ndef get_requirements(filename):\n    return open('requirements/' + filename).read().splitlines()\n\n\nclasses = \"\"\"\n    Development Status :: 4 - Beta\n    Intended Audience :: Developers\n    License :: OSI Approved :: BSD License\n    Topic :: System :: Distributed Computing\n    Programming Language :: Python\n    Programming Language :: Python :: 3\n    Programming Language :: Python :: 3 :: Only\n    Programming Language :: Python :: 3.7\n    Programming Language :: Python :: 3.8\n    Programming Language :: Python :: 3.9\n    Programming Language :: Python :: 3.10\n    Programming Language :: Python :: 3.11\n    Programming Language :: Python :: 3.12\n    Programming Language :: Python :: Implementation :: CPython\n    Programming Language :: Python :: Implementation :: PyPy\n    Operating System :: OS Independent\n\"\"\"\nclassifiers = [s.strip() for s in classes.split('\\n') if s]\n\n\nsetup(\n    name='flower',\n    version=get_package_version(),\n    description='Celery Flower',\n    long_description=open('README.rst').read(),\n    long_description_content_type=\"text/x-rst\",\n    author='Mher Movsisyan',\n    author_email='mher.movsisyan@gmail.com',\n    url='https://github.com/mher/flower',\n    license='BSD',\n    classifiers=classifiers,\n    python_requires=\">=3.7\",\n    packages=find_packages(exclude=['tests', 'tests.*']),\n    install_requires=get_requirements('default.txt'),\n    test_suite=\"tests\",\n    tests_require=get_requirements('test.txt'),\n    package_data={'flower': ['templates/*', 'static/*.*',\n                             'static/**/*.*', 'static/**/**/*.*']},\n    entry_points={\n        'celery.commands': [\n            'flower = flower.command:flower',\n        ],\n    },\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/call-tasks.sh",
    "content": "#!/bin/bash\nset -e\n\nexport ROOT_DIR=$(git rev-parse --show-toplevel)\nexport PYTHONPATH=\"$PYTHONPATH:$ROOT_DIR/examples\"\n\nN=${1:-10}\n\nfor i in $(seq 1 $N); do\n    celery -A tasks call tasks.add --args=\"[$i,$i]\";\ndone\n\ncelery -A tasks call tasks.sleep --args='[100]'\ncelery -A tasks call tasks.error --args='[10]'\n"
  },
  {
    "path": "tests/load.py",
    "content": "import time\nimport random\n\nimport os\nimport sys\ntests_dir = os.path.dirname(__file__)\nexamples_dir = os.path.join(tests_dir, '../examples')\nexamples_dir = os.path.realpath(examples_dir)\nsys.path.insert(0, examples_dir)\n\nfrom tasks import add, sleep, error\n\n\ndef main():\n    while True:\n        for i in range(10):\n            add.delay(i, i)\n        sleep.delay(random.randint(1, i))\n        error.delay(\"Something went wrong\")\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except KeyboardInterrupt:\n        pass"
  },
  {
    "path": "tests/run-unit-tests.sh",
    "content": "#!/bin/bash\nset -e\n\npython -m tests.unit\n"
  },
  {
    "path": "tests/unit/__init__.py",
    "content": "from unittest.mock import patch\nfrom urllib.parse import urlencode\n\nimport celery\nimport tornado.testing\nfrom tornado.ioloop import IOLoop\nfrom tornado.options import options\n\nfrom flower import command  # noqa: F401 side effect - define options\nfrom flower.app import Flower\nfrom flower.events import Events\nfrom flower.urls import handlers, settings\n\n\nclass AsyncHTTPTestCase(tornado.testing.AsyncHTTPTestCase):\n\n    def _get_celery_app(self):\n        return celery.Celery()\n\n    def get_app(self, capp=None):\n        if not capp:\n            capp = self._get_celery_app()\n        events = Events(capp, IOLoop.current())\n        app = Flower(capp=capp, events=events,\n                     options=options, handlers=handlers, **settings)\n        return app\n\n    def get(self, url, **kwargs):\n        return self.fetch(url, **kwargs)\n\n    def post(self, url, **kwargs):\n        if 'body' in kwargs and isinstance(kwargs['body'], dict):\n            kwargs['body'] = urlencode(kwargs['body'])\n        return self.fetch(url, method='POST', **kwargs)\n\n    def mock_option(self, name, value):\n        return patch.object(options.mockable(), name, value)\n"
  },
  {
    "path": "tests/unit/__main__.py",
    "content": "import unittest\nfrom glob import glob\n\nimport tornado.testing\n\n\ndef all():\n    test_modules = list(map(lambda x: x.rstrip('.py').replace('/', '.'),\n                            glob('tests/unit/*.py') + glob('tests/unit/**/*.py')))\n    return unittest.defaultTestLoader.loadTestsFromNames(test_modules)\n\n\nif __name__ == \"__main__\":\n    tornado.testing.main()\n"
  },
  {
    "path": "tests/unit/api/__init__.py",
    "content": "import os\n\nfrom tests.unit import AsyncHTTPTestCase\n\n\nclass BaseApiTestCase(AsyncHTTPTestCase):\n    def setUp(self):\n        super().setUp()\n        os.environ['FLOWER_UNAUTHENTICATED_API'] = 'true'\n\n    def tearDown(self):\n        super().tearDown()\n        del os.environ['FLOWER_UNAUTHENTICATED_API']\n"
  },
  {
    "path": "tests/unit/api/test_auth.py",
    "content": "from tests.unit import AsyncHTTPTestCase\n\n\nclass BasicAuthTests(AsyncHTTPTestCase):\n    def test_auth(self):\n        with self.mock_option('basic_auth', ['user1:pswd1', 'user2:pswd2']):\n            r = self.fetch('/api/workers')\n            self.assertEqual(401, r.code)\n            r = self.fetch('/api/workers', auth_username='user1', auth_password='pswd1')\n            self.assertEqual(200, r.code)\n            r = self.fetch('/api/workers', auth_username='user2', auth_password='pswd2')\n            self.assertEqual(200, r.code)\n            r = self.fetch('/api/workers', auth_username='user1', auth_password='pswd2')\n            self.assertEqual(401, r.code)\n"
  },
  {
    "path": "tests/unit/api/test_control.py",
    "content": "import os\nfrom unittest.mock import MagicMock, patch\n\nfrom tornado.options import options\n\nfrom flower.api.control import ControlHandler\n\nfrom . import BaseApiTestCase\n\n\nclass UnknownWorkerControlTests(BaseApiTestCase):\n    def test_unknown_worker(self):\n        r = self.post('/api/worker/shutdown/test', body={})\n        self.assertEqual(404, r.code)\n\n\nclass WorkerControlTests(BaseApiTestCase):\n    def setUp(self):\n        BaseApiTestCase.setUp(self)\n        self.is_worker = ControlHandler.is_worker\n        ControlHandler.is_worker = lambda *args: True\n\n    def tearDown(self):\n        BaseApiTestCase.tearDown(self)\n        ControlHandler.is_worker = self.is_worker\n\n    def test_shutdown(self):\n        celery = self._app.capp\n        celery.control.broadcast = MagicMock()\n        r = self.post('/api/worker/shutdown/test', body={})\n        self.assertEqual(200, r.code)\n        celery.control.broadcast.assert_called_once_with('shutdown',\n                                                         destination=['test'])\n\n    def test_pool_restart(self):\n        celery = self._app.capp\n        celery.control.broadcast = MagicMock(return_value=[{'test': 'ok'}])\n        r = self.post('/api/worker/pool/restart/test', body={})\n        self.assertEqual(200, r.code)\n        celery.control.broadcast.assert_called_once_with(\n            'pool_restart',\n            arguments={'reload': False},\n            destination=['test'],\n            reply=True,\n        )\n\n    def test_pool_grow(self):\n        celery = self._app.capp\n        celery.control.pool_grow = MagicMock(return_value=[{'test': 'ok'}])\n        r = self.post('/api/worker/pool/grow/test', body={'n': 3})\n        self.assertEqual(200, r.code)\n        celery.control.pool_grow.assert_called_once_with(\n            n=3, reply=True, destination=['test'])\n\n    def test_pool_shrink(self):\n        celery = self._app.capp\n        celery.control.pool_shrink = MagicMock(return_value=[{'test': 'ok'}])\n        r = self.post('/api/worker/pool/shrink/test', body={})\n        self.assertEqual(200, r.code)\n        celery.control.pool_shrink.assert_called_once_with(\n            n=1, reply=True, destination=['test'])\n\n    def test_pool_autoscale(self):\n        celery = self._app.capp\n        celery.control.broadcast = MagicMock(return_value=[{'test': 'ok'}])\n        r = self.post('/api/worker/pool/autoscale/test',\n                      body={'min': 2, 'max': 5})\n        self.assertEqual(200, r.code)\n        celery.control.broadcast.assert_called_once_with(\n            'autoscale',\n            reply=True, destination=['test'],\n            arguments={'min': 2, 'max': 5})\n\n    def test_add_consumer(self):\n        celery = self._app.capp\n        celery.control.broadcast = MagicMock(\n            return_value=[{'test': {'ok': ''}}])\n        r = self.post('/api/worker/queue/add-consumer/test',\n                      body={'queue': 'foo'})\n        self.assertEqual(200, r.code)\n        celery.control.broadcast.assert_called_once_with(\n            'add_consumer',\n            reply=True, destination=['test'],\n            arguments={'queue': 'foo'})\n\n    def test_cancel_consumer(self):\n        celery = self._app.capp\n        celery.control.broadcast = MagicMock(\n            return_value=[{'test': {'ok': ''}}])\n        r = self.post('/api/worker/queue/cancel-consumer/test',\n                      body={'queue': 'foo'})\n        self.assertEqual(200, r.code)\n        celery.control.broadcast.assert_called_once_with(\n            'cancel_consumer',\n            reply=True, destination=['test'],\n            arguments={'queue': 'foo'})\n\n    def test_task_timeout(self):\n        celery = self._app.capp\n        celery.control.time_limit = MagicMock(\n            return_value=[{'foo': {'ok': ''}}])\n\n        r = self.post(\n            '/api/task/timeout/celery.map',\n            body={'workername': 'foo', 'hard': 3.1, 'soft': 1.2}\n        )\n        self.assertEqual(200, r.code)\n        celery.control.time_limit.assert_called_once_with(\n            'celery.map', hard=3.1, soft=1.2, destination=['foo'],\n            reply=True)\n\n    def test_task_ratelimit(self):\n        celery = self._app.capp\n        celery.control.rate_limit = MagicMock(\n            return_value=[{'foo': {'ok': ''}}])\n\n        r = self.post('/api/task/rate-limit/celery.map',\n                      body={'workername': 'foo', 'ratelimit': 20})\n        self.assertEqual(200, r.code)\n        celery.control.rate_limit.assert_called_once_with(\n            'celery.map', '20', destination=['foo'], reply=True)\n\n    def test_task_ratelimit_non_integer(self):\n        celery = self._app.capp\n        celery.control.rate_limit = MagicMock(\n            return_value=[{'foo': {'ok': ''}}])\n\n        r = self.post('/api/task/rate-limit/celery.map',\n                      body={'workername': 'foo', 'ratelimit': '11/m'})\n        self.assertEqual(200, r.code)\n        celery.control.rate_limit.assert_called_once_with(\n            'celery.map', '11/m', destination=['foo'], reply=True)\n\n    def test_param_escape(self):\n        app = self._app.capp\n        app.control.broadcast = MagicMock(\n            return_value=[{'test': {'ok': ''}}])\n        r = self.post('/api/worker/queue/add-consumer/test',\n                      body={'queue': 'foo&bar'})\n        self.assertEqual(200, r.code)\n        app.control.broadcast.assert_called_once_with(\n            'add_consumer',\n            reply=True, destination=['test'],\n            arguments={'queue': 'foo&amp;bar'})\n\n\nclass TaskControlTests(BaseApiTestCase):\n    def test_revoke(self):\n        celery = self._app.capp\n        celery.control.revoke = MagicMock()\n        r = self.post('/api/task/revoke/test', body={})\n        self.assertEqual(200, r.code)\n        celery.control.revoke.assert_called_once_with('test',\n                                                      terminate=False,\n                                                      signal='SIGTERM')\n\n    def test_terminate(self):\n        celery = self._app.capp\n        celery.control.revoke = MagicMock()\n        r = self.post('/api/task/revoke/test', body={'terminate': True})\n        self.assertEqual(200, r.code)\n        celery.control.revoke.assert_called_once_with('test',\n                                                      terminate=True,\n                                                      signal='SIGTERM')\n\n    def test_terminate_signal(self):\n        celery = self._app.capp\n        celery.control.revoke = MagicMock()\n        r = self.post('/api/task/revoke/test',\n                      body={'terminate': True, 'signal': 'SIGUSR1'})\n        self.assertEqual(200, r.code)\n        celery.control.revoke.assert_called_once_with('test',\n                                                      terminate=True,\n                                                      signal='SIGUSR1')\n\n\nclass ControlAuthTests(WorkerControlTests):\n    def test_auth(self):\n        with patch.object(options.mockable(), 'basic_auth', ['user1:password1']):\n            app = self._app.capp\n            app.control.broadcast = MagicMock()\n            r = self.post('/api/worker/shutdown/test', body={})\n            self.assertEqual(401, r.code)\n\n    @patch.dict(os.environ, {'FLOWER_UNAUTHENTICATED_API': ''})\n    def test_auth_without_env_var(self):\n        app = self._app.capp\n        app.control.broadcast = MagicMock()\n        r = self.post('/api/worker/shutdown/test', body={})\n        self.assertEqual(401, r.code)\n"
  },
  {
    "path": "tests/unit/api/test_tasks.py",
    "content": "import json\nimport time\nfrom collections import OrderedDict\nfrom datetime import datetime, timedelta\nfrom unittest.mock import Mock, PropertyMock, patch\n\nimport celery.states as states\nfrom celery.events import Event\nfrom celery.result import AsyncResult\n\nfrom flower.events import EventsState\nfrom tests.unit.utils import task_succeeded_events\n\nfrom . import BaseApiTestCase\n\n\nclass ApplyTests(BaseApiTestCase):\n    def test_apply(self):\n        result = 'result'\n        with patch('celery.result.AsyncResult.state', new_callable=PropertyMock) as mock_state:\n            with patch('celery.result.AsyncResult.result', new_callable=PropertyMock) as mock_result:\n                mock_state.return_value = states.SUCCESS\n                mock_result.return_value = result\n\n                ar = AsyncResult(123)\n                ar.get = Mock(return_value=result)\n\n                task = self._app.capp.tasks['foo'] = Mock()\n                task.apply_async = Mock(return_value=ar)\n\n                r = self.post('/api/task/apply/foo', body='')\n\n        self.assertEqual(200, r.code)\n        body = bytes.decode(r.body)\n        self.assertEqual(result, json.loads(body)['result'])\n        task.apply_async.assert_called_once_with(args=[], kwargs={})\n\n\nclass AsyncApplyTests(BaseApiTestCase):\n    def test_async_apply(self):\n        task = self._app.capp.tasks['foo'] = Mock()\n        task.apply_async = Mock(return_value=AsyncResult(123))\n        r = self.post('/api/task/async-apply/foo', body={})\n\n        self.assertEqual(200, r.code)\n        task.apply_async.assert_called_once_with(args=[], kwargs={})\n\n    def test_async_apply_eta(self):\n        task = self._app.capp.tasks['foo'] = Mock()\n        task.apply_async = Mock(return_value=AsyncResult(123))\n        tomorrow = datetime.utcnow() + timedelta(days=1)\n        r = self.post('/api/task/async-apply/foo',\n                      body='{\"eta\": \"%s\"}' % tomorrow)\n\n        self.assertEqual(200, r.code)\n        task.apply_async.assert_called_once_with(\n            args=[], kwargs={}, eta=tomorrow)\n\n    def test_async_apply_countdown(self):\n        task = self._app.capp.tasks['foo'] = Mock()\n        task.apply_async = Mock(return_value=AsyncResult(123))\n        r = self.post('/api/task/async-apply/foo',\n                      body='{\"countdown\": \"3\"}')\n\n        self.assertEqual(200, r.code)\n        task.apply_async.assert_called_once_with(\n            args=[], kwargs={}, countdown=3)\n\n    def test_async_apply_expires(self):\n        task = self._app.capp.tasks['foo'] = Mock()\n        task.apply_async = Mock(return_value=AsyncResult(123))\n        r = self.post('/api/task/async-apply/foo',\n                      body='{\"expires\": \"60\"}')\n\n        self.assertEqual(200, r.code)\n        task.apply_async.assert_called_once_with(\n            args=[], kwargs={}, expires=60)\n\n    def test_async_apply_expires_datetime(self):\n        task = self._app.capp.tasks['foo'] = Mock()\n        task.apply_async = Mock(return_value=AsyncResult(123))\n        tomorrow = datetime.utcnow() + timedelta(days=1)\n        r = self.post('/api/task/async-apply/foo',\n                      body='{\"expires\": \"%s\"}' % tomorrow)\n\n        self.assertEqual(200, r.code)\n        task.apply_async.assert_called_once_with(\n            args=[], kwargs={}, expires=tomorrow)\n\n\nclass MockTasks:\n\n    @staticmethod\n    def get_task_by_id(events, task_id):\n        from celery.events.state import Task\n        return Task()\n\n\nclass TaskTests(BaseApiTestCase):\n    def setUp(self):\n        self.app = super().get_app()\n        super().setUp()\n\n    def get_app(self, capp=None):\n        return self.app\n\n    @patch('flower.api.tasks.tasks', new=MockTasks)\n    def test_task_info(self):\n        self.get('/api/task/info/123')\n\n    def test_tasks_pagination(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='123')\n        events += task_succeeded_events(worker='worker1', name='task2',\n                                        id='456')\n        events += task_succeeded_events(worker='worker1', name='task3',\n                                        id='789')\n        events += task_succeeded_events(worker='worker1', name='task4',\n                                        id='666')\n\n        # for i, e in enumerate(sorted(events, key=lambda event: event['uuid'])):\n\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        # Test limit 4 and offset 0\n        params = dict(limit=4, offset=0, sort_by='name')\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(4, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        lastFetchedTaskName = table[list(table)[-1]]['name']\n        self.assertEqual(\"task1\", firstFetchedTaskName)\n        self.assertEqual(\"task4\", lastFetchedTaskName)\n\n        # Test limit 4 and offset 1\n        params = dict(limit=4, offset=1, sort_by='name')\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(3, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        lastFetchedTaskName = table[list(table)[-1]]['name']\n        self.assertEqual(\"task2\", firstFetchedTaskName)\n        self.assertEqual(\"task4\", lastFetchedTaskName)\n\n        # Test limit 4 and offset -1 (-1 should act as 0)\n        params = dict(limit=4, offset=-1, sort_by=\"name\")\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(4, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        lastFetchedTaskName = table[list(table)[-1]]['name']\n        self.assertEqual(\"task1\", firstFetchedTaskName)\n        self.assertEqual(\"task4\", lastFetchedTaskName)\n\n        # Test limit 2 and offset 1\n        params = dict(limit=2, offset=1, sort_by='name')\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        lastFetchedTaskName = table[list(table)[-1]]['name']\n        self.assertEqual(\"task2\", firstFetchedTaskName)\n        self.assertEqual(\"task3\", lastFetchedTaskName)\n\n        # Test limit 4 with search\n        params = dict(limit=4, offset=0, sort_by='name', search='task')\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(4, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        lastFetchedTaskName = table[list(table)[-1]]['name']\n        self.assertEqual(\"task1\", firstFetchedTaskName)\n        self.assertEqual(\"task4\", lastFetchedTaskName)\n\n        # Test limit 4 with search\n        params = dict(limit=4, offset=0, sort_by='name', search='task1')\n\n        r = self.get('/api/tasks?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"), object_pairs_hook=OrderedDict)\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(1, len(table))\n        firstFetchedTaskName = table[list(table)[0]]['name']\n        self.assertEqual(\"task1\", firstFetchedTaskName)\n"
  },
  {
    "path": "tests/unit/api/test_workers.py",
    "content": "import json\nfrom unittest import mock\n\nfrom flower.inspector import Inspector\n\nfrom . import BaseApiTestCase\n\ninspect_response = {\n    'celery@worker1':  [\n        \"tasks.add\",\n        \"tasks.sleep\"\n    ],\n}\n\nempty_inspect_response = {\n    'celery@worker1': []\n}\n\n\n@mock.patch.object(Inspector, 'methods',\n                   new_callable=mock.PropertyMock,\n                   return_value=['inspect_method'])\nclass ListWorkersTest(BaseApiTestCase):\n\n    def test_refresh_cache(self, m_inspect):\n        celery = self._app.capp\n        celery.control.inspect = mock.Mock()\n        celery.control.inspect.return_value.inspect_method = mock.Mock(\n            return_value=inspect_response\n        )\n\n        r = self.get('/api/workers?refresh=1')\n        celery.control.inspect.assert_called_once_with(\n            timeout=1,\n            destination=None\n        )\n\n        body = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(\n            inspect_response['celery@worker1'],\n            body['celery@worker1']['inspect_method']\n        )\n        self.assertIn('timestamp', body['celery@worker1'])\n        self.assertEqual(\n            inspect_response['celery@worker1'],\n            self._app.workers['celery@worker1']['inspect_method']\n        )\n\n    def test_refresh_cache_with_empty_response(self, m_inspect):\n        celery = self._app.capp\n        celery.control.inspect = mock.Mock()\n        celery.control.inspect.return_value.inspect_method = mock.Mock(\n            return_value=inspect_response\n        )\n        r = self.get('/api/workers?refresh=1')\n\n        celery.control.inspect.return_value.inspect_method = mock.Mock(\n            return_value=empty_inspect_response\n        )\n\n        r = self.get('/api/workers?refresh=1')\n\n        body = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(\n            [],\n            body['celery@worker1']['inspect_method']\n        )\n        self.assertIn('timestamp', body['celery@worker1'])\n        self.assertEqual(\n            [],\n            self._app.workers['celery@worker1']['inspect_method']\n        )\n"
  },
  {
    "path": "tests/unit/test_command.py",
    "content": "import os\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport celery\nfrom prometheus_client import Histogram\nfrom tornado.options import options\n\nfrom flower.command import (apply_env_options, apply_options, print_banner,\n                            warn_about_celery_args_used_in_flower_command)\nfrom tests.unit import AsyncHTTPTestCase\n\n\nclass TestFlowerCommand(AsyncHTTPTestCase):\n    def test_task_runtime_metric_buckets_read_from_cmd_line(self):\n        apply_options('flower', argv=['--task_runtime_metric_buckets=1,10,inf'])\n        self.assertEqual([1.0, 10.0, float('inf')], options.task_runtime_metric_buckets)\n\n    def test_task_runtime_metric_buckets_no_cmd_line_arg(self):\n        apply_options('flower', argv=[])\n        self.assertEqual(Histogram.DEFAULT_BUCKETS, options.task_runtime_metric_buckets)\n\n    def test_task_runtime_metric_buckets_read_from_env(self):\n        with patch.dict(os.environ, {\"FLOWER_TASK_RUNTIME_METRIC_BUCKETS\": \"2,5,inf\"}):\n            apply_env_options()\n            self.assertEqual([2.0, 5.0, float('inf')], options.task_runtime_metric_buckets)\n\n    def test_task_runtime_metric_buckets_no_env_value_provided(self):\n        apply_env_options()\n        self.assertEqual(Histogram.DEFAULT_BUCKETS, options.task_runtime_metric_buckets)\n\n    def test_port(self):\n        with self.mock_option('port', 5555):\n            apply_options('flower', argv=['--port=123'])\n            self.assertEqual(123, options.port)\n\n    def test_address(self):\n        with self.mock_option('address', '127.0.0.1'):\n            apply_options('flower', argv=['--address=foo'])\n            self.assertEqual('foo', options.address)\n\n    def test_auto_refresh(self):\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"false\"}):\n            apply_env_options()\n            self.assertFalse(options.auto_refresh)\n\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"true\"}):\n            apply_env_options()\n            self.assertTrue(options.auto_refresh)\n\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"0\"}):\n            apply_env_options()\n            self.assertFalse(options.auto_refresh)\n\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"1\"}):\n            apply_env_options()\n            self.assertTrue(options.auto_refresh)\n\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"False\"}):\n            apply_env_options()\n            self.assertFalse(options.auto_refresh)\n\n        with patch.dict(os.environ, {\"FLOWER_AUTO_REFRESH\": \"True\"}):\n            apply_env_options()\n            self.assertTrue(options.auto_refresh)\n\n    def test_autodiscovery(self):\n        \"\"\"\n        Simulate basic Django setup:\n        - creating celery app\n        - run app.autodiscover_tasks()\n        - create flower command\n        \"\"\"\n        celery_app = self._get_celery_app()\n        with patch.object(celery_app, '_autodiscover_tasks') as autodiscover:\n            celery_app.autodiscover_tasks()\n\n            self.get_app(capp=celery_app)\n\n            self.assertTrue(autodiscover.called)\n\n\nclass TestPrintBanner(AsyncHTTPTestCase):\n    def test_print_banner(self):\n        celery_app = celery.Celery()\n        with self.assertLogs('', level='INFO') as cm:\n            print_banner(celery_app, False)\n\n            self.assertTrue('INFO:flower.command:Visit me at http://0.0.0.0:5555' in cm.output)\n            self.assertTrue('INFO:flower.command:Broker: amqp://guest:**@localhost:5672//' in cm.output)\n\n    def test_print_banner_with_ssl(self):\n        celery_app = celery.Celery()\n        with self.assertLogs('', level='INFO') as cm:\n            print_banner(celery_app, True)\n\n            self.assertTrue('INFO:flower.command:Visit me at https://0.0.0.0:5555' in cm.output)\n            self.assertTrue('INFO:flower.command:Broker: amqp://guest:**@localhost:5672//' in cm.output)\n\n    def test_print_banner_unix_socket(self):\n        celery_app = celery.Celery()\n        with self.assertLogs('', level='INFO') as cm, self.mock_option('unix_socket', 'foo'):\n            print_banner(celery_app, True)\n\n            self.assertTrue('INFO:flower.command:Visit me via unix socket file: foo' in cm.output)\n\n\nclass TestWarnAboutCeleryArgsUsedInFlowerCommand(AsyncHTTPTestCase):\n    @patch('flower.command.logger.warning')\n    def test_does_not_log_warning(self, mock_warning):\n        mock_app_param = Mock(name='app_param', opts=('-A', '--app'))\n        mock_broker_param = Mock(name='broker_param', opts=('-b', '--broker'))\n\n        class FakeContext:\n            parent = Mock(command=Mock(params=[mock_app_param, mock_broker_param]))\n\n        ctx = FakeContext()\n\n        warn_about_celery_args_used_in_flower_command(\n            ctx=ctx, flower_args=('--port=5678', '--address=0.0.0.0')\n        )\n\n        mock_warning.assert_not_called()\n\n    @patch('flower.command.logger.warning')\n    def test_logs_warning(self, mock_warning):\n        mock_app_param = Mock(name='app_param', opts=('-A', '--app'))\n        mock_broker_param = Mock(name='broker_param', opts=('-b', '--broker'))\n\n        class FakeContext:\n            parent = Mock(command=Mock(params=[mock_app_param, mock_broker_param]))\n\n        ctx = FakeContext()\n\n        warn_about_celery_args_used_in_flower_command(\n            ctx=ctx, flower_args=('--app=proj', '-b', 'redis://localhost:6379/0')\n        )\n\n        mock_warning.assert_called_once_with(\n            \"You have incorrectly specified the following celery arguments after flower command: \"\n            \"%s. Please specify them after celery command instead following\"\n            \" this template: celery [celery args] flower [flower args].\", ['--app', '-b']\n        )\n\n\nclass TestConfOption(AsyncHTTPTestCase):\n    def test_error_conf(self):\n        with self.mock_option('conf', None):\n            self.assertRaises(IOError, apply_options,\n                              'flower', argv=['--conf=foo'])\n            self.assertRaises(IOError, apply_options,\n                              'flower', argv=['--conf=/tmp/flower/foo'])\n\n    def test_default_option(self):\n        apply_options('flower', argv=[])\n        self.assertEqual('flowerconfig.py', options.conf)\n\n    def test_empty_conf(self):\n        with self.mock_option('conf', None):\n            apply_options('flower', argv=['--conf=/dev/null'])\n            self.assertEqual('/dev/null', options.conf)\n\n    def test_conf_abs(self):\n        with tempfile.NamedTemporaryFile() as cf:\n            with self.mock_option('conf', cf.name), self.mock_option('debug', False):\n                cf.write('debug=True\\n'.encode('utf-8'))\n                cf.flush()\n                apply_options('flower', argv=['--conf=%s' % cf.name])\n                self.assertEqual(cf.name, options.conf)\n                self.assertTrue(options.debug)\n\n    def test_conf_relative(self):\n        with tempfile.NamedTemporaryFile(dir='.') as cf:\n            with self.mock_option('conf', cf.name), self.mock_option('debug', False):\n                cf.write('debug=True\\n'.encode('utf-8'))\n                cf.flush()\n                apply_options('flower', argv=['--conf=%s' % os.path.basename(cf.name)])\n                self.assertTrue(options.debug)\n\n    @unittest.skipUnless(not sys.platform.startswith(\"win\"), 'skip windows')\n    def test_all_options_documented(self):\n        def grep(patter, filename):\n            return int(subprocess.check_output(\n                'grep \"%s\" %s|wc -l' % (patter, filename), shell=True))\n\n        defined = grep('^define(', 'flower/options.py')\n        documented = grep('^~~', 'docs/config.rst')\n        self.assertEqual(defined, documented,\n                         msg='Missing option documentation. Make sure all options '\n                             'are documented in docs/config.rst')\n"
  },
  {
    "path": "tests/unit/utils/__init__.py",
    "content": "import xml.etree.ElementTree as ET\nfrom html.parser import HTMLParser\n\nfrom celery.events import Event\nfrom celery.utils import uuid\n\n\nclass HtmlTableParser(HTMLParser):\n    def __init__(self, *args, **kwargs):\n        HTMLParser.__init__(self, *args, **kwargs)\n        self.table = ''\n        self.inTable = False\n\n    def handle_starttag(self, tag, attrs):\n        if tag == 'table':\n            self.inTable = True\n        if self.inTable:\n            self.table += '<%s' % tag\n            for attr in attrs:\n                self.table += ' %s=\"%s\"' % attr\n            self.table += '>'\n\n    def handle_endtag(self, tag):\n        if self.inTable:\n            self.table += '</%s>' % tag\n            if tag == 'table':\n                self.inTable = False\n\n    def handle_data(self, data):\n        if self.inTable:\n            self.table += data\n\n    def parse(self, source):\n        self.feed(source)\n\n    def query(self, pattern):\n        root = ET.fromstring(self.table)\n        return root.findall(pattern)\n\n    def rows(self):\n        return self.query('tbody/tr')\n\n    def get_row(self, row_id):\n        rows = self.query('tbody/tr')\n        for r in rows:\n            if r.attrib.get('id') == row_id:\n                cells = r.findall('td')\n                return list(map(lambda x: getattr(x, 'text'), cells))\n\n\ndef task_succeeded_events(worker, id=None, name=None, runtime=0.1234, retries=0, eta=None):\n    id = id or uuid()\n    name = name or 'sometask'\n    return [Event('task-received', uuid=id, name=name,\n                  args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                  retries=retries, eta=eta, hostname=worker),\n            Event('task-started', uuid=id, hostname=worker),\n            Event('task-succeeded', uuid=id, result='4',\n                  runtime=runtime, hostname=worker)]\n\n\ndef task_failed_events(worker, id=None, name=None):\n    id = id or uuid()\n    name = name or 'sometask'\n    return [Event('task-received', uuid=id, name=name,\n                  args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                  retries=0, eta=None, hostname=worker),\n            Event('task-started', uuid=id, hostname=worker),\n            Event('task-failed', uuid=id, exception=\"KeyError('foo')\",\n                  traceback='line 1 at main', hostname=worker)]\n"
  },
  {
    "path": "tests/unit/utils/test_broker.py",
    "content": "import unittest\nfrom unittest.mock import MagicMock\n\nfrom flower.utils import broker\nfrom flower.utils.broker import (Broker, RabbitMQ, Redis, RedisBase,\n                                 RedisSentinel, RedisSocket)\n\nbroker.requests = MagicMock()\nbroker.redis = MagicMock()\n\n\nclass TestRabbitMQ(unittest.TestCase):\n    def test_init(self):\n        for url in ['amqp://', 'amqps://']:\n            b = Broker(url, '')\n            self.assertTrue(isinstance(b, RabbitMQ))\n            self.assertFalse(isinstance(b, Redis))\n\n    def test_url(self):\n        b = RabbitMQ('amqp://user:pass@host:10000/vhost', '')\n        self.assertEqual('host', b.host)\n        self.assertEqual(10000, b.port)\n        self.assertEqual('vhost', b.vhost)\n        self.assertEqual('user', b.username)\n        self.assertEqual('pass', b.password)\n\n    def test_url_vhost_slash(self):\n        b = RabbitMQ('amqp://user:pass@host:10000//', '')\n        self.assertEqual('host', b.host)\n        self.assertEqual(10000, b.port)\n        self.assertEqual('/', b.vhost)\n        self.assertEqual('user', b.username)\n        self.assertEqual('pass', b.password)\n\n    def test_url_defaults_rabbitmq(self):\n        for url in ['amqp://', 'amqp://localhost', 'amqps://', 'amqps://localhost']:\n            b = RabbitMQ(url, '')\n            self.assertEqual('localhost', b.host)\n            self.assertEqual(15672, b.port)\n            self.assertEqual('/', b.vhost)\n            self.assertEqual('guest', b.username)\n            self.assertEqual('guest', b.password)\n\n    def test_url_defaults_redis(self):\n        for url in ['redis://', 'redis://localhost', 'redis://localhost/0']:\n            b = Redis(url, '')\n            self.assertEqual('localhost', b.host)\n            self.assertEqual(6379, b.port)\n            self.assertEqual(0, b.vhost)\n            self.assertEqual(None, b.username)\n            self.assertEqual(None, b.password)\n\n    def test_invalid_http_api(self):\n        with self.assertLogs('', level='ERROR') as cm:\n            RabbitMQ('amqp://user:pass@host:10000/vhost', http_api='ftp://')\n            self.assertEqual(['ERROR:flower.utils.broker:Invalid broker api url: ftp://'], cm.output)\n\n\nclass TestRedis(unittest.TestCase):\n    def test_init(self):\n        b = Broker('redis://localhost:6379/0')\n        self.assertFalse(isinstance(b, RabbitMQ))\n        self.assertTrue(isinstance(b, Redis))\n\n    def test_priority_steps(self):\n        custom_steps = list(range(10))\n        cases = [(RedisBase.DEFAULT_PRIORITY_STEPS, {}),\n                 (custom_steps, {'priority_steps': custom_steps})]\n        for expected, options in cases:\n            b = Broker('redis://localhost:6379/0', broker_options=options)\n            self.assertEqual(expected, b.priority_steps)\n\n    def test_custom_sep(self):\n        custom_sep = '.'\n        cases = [(RedisBase.DEFAULT_SEP, {}),\n                 (custom_sep, {'sep': custom_sep})]\n        for expected, options in cases:\n            b = Broker('redis://localhost:6379/0', broker_options=options)\n            self.assertEqual(expected, b.sep)\n\n    def test_url(self):\n        b = Broker('redis://foo:7777/9')\n        self.assertEqual('foo', b.host)\n        self.assertEqual(7777, b.port)\n        self.assertEqual(9, b.vhost)\n\n    def test_url_defaults(self):\n        b = Broker('redis://')\n        self.assertEqual('localhost', b.host)\n        self.assertEqual(6379, b.port)\n        self.assertEqual(0, b.vhost)\n        self.assertIsNone(b.username)\n        self.assertIsNone(b.password)\n\n    def test_url_with_password(self):\n        b = Broker('redis://:pass@host:4444/5')\n        self.assertEqual('host', b.host)\n        self.assertEqual(4444, b.port)\n        self.assertEqual(5, b.vhost)\n        self.assertEqual('pass', b.password)\n\n    def test_url_with_user_and_password(self):\n        b = Broker('redis://user:pass@host:4444/5')\n        self.assertEqual('host', b.host)\n        self.assertEqual(4444, b.port)\n        self.assertEqual(5, b.vhost)\n        self.assertEqual('user', b.username)\n        self.assertEqual('pass', b.password)\n\n    def test_ipv6(self):\n        b = Broker('redis://[::1]')\n        self.assertEqual('::1', b.host)\n        self.assertEqual(6379, b.port)\n        self.assertEqual(0, b.vhost)\n\n\nclass TestRedisSentinel(unittest.TestCase):\n    def test_init(self):\n        options = {'master_name': 'my_redis_master'}\n        b = Broker('sentinel://localhost:26379/', broker_options=options)\n        self.assertFalse(isinstance(b, RabbitMQ))\n        self.assertTrue(isinstance(b, RedisSentinel))\n\n    def test_priority_steps(self):\n        custom_steps = list(range(10))\n        cases = [(RedisBase.DEFAULT_PRIORITY_STEPS, {'master_name': 'my_redis_master'}),\n                 (custom_steps, {'master_name': 'my_redis_master', 'priority_steps': custom_steps})]\n        for expected, options in cases:\n            b = Broker('sentinel://localhost:6379/0', broker_options=options)\n            self.assertEqual(expected, b.priority_steps)\n\n    def test_url(self):\n        options = {'master_name': 'my_redis_master'}\n        b = Broker('sentinel://foo:7777/9', broker_options=options)\n        self.assertEqual('foo', b.host)\n        self.assertEqual(7777, b.port)\n        self.assertEqual(9, b.vhost)\n\n    def test_url_defaults(self):\n        options = {'master_name': 'my_redis_master'}\n        b = Broker('sentinel://', broker_options=options)\n        self.assertEqual('localhost', b.host)\n        self.assertEqual(26379, b.port)\n        self.assertEqual(0, b.vhost)\n        self.assertIsNone(b.username)\n        self.assertIsNone(b.password)\n\n    def test_url_with_password(self):\n        options = {'master_name': 'my_redis_master'}\n        b = Broker('sentinel://:pass@host:4444/5', broker_options=options)\n        self.assertEqual('host', b.host)\n        self.assertEqual(4444, b.port)\n        self.assertEqual(5, b.vhost)\n        self.assertEqual('pass', b.password)\n\n\nclass TestRedisSsl(unittest.TestCase):\n\n    BROKER_USE_SSL_OPTIONS = {\n        'ssl_cert_reqs': 0,\n        'ssl_certfile': '/path/to/ssl_cert_file',\n        'ssl_keyfile': '/path/to/ssl_key_file',\n    }\n\n    def test_init_with_broker_use_ssl(self):\n        b = Broker('rediss://localhost:6379/0', broker_use_ssl=self.BROKER_USE_SSL_OPTIONS)\n        self.assertFalse(isinstance(b, RabbitMQ))\n        self.assertTrue(isinstance(b, Redis))\n\n    def test_init_without_broker_use_ssl(self):\n        with self.assertRaises(ValueError):\n            Broker('rediss://localhost:6379/0')\n\n    def test_redis_client_args(self):\n        b = Broker('rediss://:pass@host:4444/5', broker_use_ssl=self.BROKER_USE_SSL_OPTIONS)\n        self.assertEqual('host', b.host)\n        self.assertEqual(4444, b.port)\n        self.assertEqual(5, b.vhost)\n        self.assertEqual('pass', b.password)\n\n        redis_client_args = b._get_redis_client_args()\n        for ssl_key, ssl_val in self.BROKER_USE_SSL_OPTIONS.items():\n            self.assertIn(ssl_key, redis_client_args)\n            self.assertEqual(ssl_val, redis_client_args[ssl_key])\n\n\nclass TestRedisSocket(unittest.TestCase):\n    def test_init(self):\n        b = Broker('redis+socket:///path/to/socket')\n        self.assertFalse(isinstance(b, RabbitMQ))\n        self.assertTrue(isinstance(b, RedisSocket))\n\n    def test_url(self):\n        b = Broker('redis+socket:///path/to/socket')\n        self.assertEqual(None, b.host)\n        self.assertEqual(None, b.port)\n        self.assertEqual('path/to/socket', b.vhost)\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/unit/utils/test_search.py",
    "content": "import unittest\nfrom collections import namedtuple\n\nfrom flower.utils.search import (parse_search_terms, satisfies_search_terms,\n                                 stringified_dict_contains_value)\n\n\nclass TestSearchParser(unittest.TestCase):\n    def test_any_value(self):\n        self.assertEqual(\n            {'any': 'someval'},\n            parse_search_terms('someval')\n        )\n\n    def test_result_value(self):\n        self.assertEqual(\n            {'result': 'resval'},\n            parse_search_terms('result:resval')\n        )\n\n    def test_kwargs(self):\n        self.assertEqual(\n            {'kwargs': {'some_kwarg': 'some_value'}},\n            parse_search_terms('kwargs:some_kwarg=some_value')\n        )\n        self.assertEqual(\n            {'kwargs': {'some_kwarg1': 'some_value1', 'some_kwarg2': 'some_value2'}},\n            parse_search_terms('kwargs:some_kwarg1=some_value1 kwargs:some_kwarg2=some_value2')\n        )\n\n    def test_partial_kwargs(self):\n        self.assertEqual(\n            {'kwargs': {}},\n            parse_search_terms('kwargs:some_kwarg')\n        )\n        self.assertEqual(\n            {'kwargs': {'some_kwarg': ''}},\n            parse_search_terms('kwargs:some_kwarg=')\n        )\n\n    def test_args(self):\n        self.assertEqual(\n            {'args': ['some_value']},\n            parse_search_terms('args:some_value')\n        )\n        self.assertEqual(\n            {'args': ['some_value1', 'some_value2']},\n            parse_search_terms('args:some_value1 args:some_value2')\n        )\n\n    def test_strip_spaces(self):\n        self.assertEqual(\n            {'any': 'someval'},\n            parse_search_terms('    someval  ')\n        )\n        self.assertEqual(\n            {'kwargs': {'some_kwarg': 'some_value'}},\n            parse_search_terms('     kwargs:some_kwarg=some_value   ')\n        )\n\n    def test_quotes(self):\n        self.assertEqual(\n            {'result': 'complex kwarg'},\n            parse_search_terms('result:\"complex kwarg\"')\n        )\n        self.assertEqual(\n            {'kwargs': {'some_kwarg1': 'some value1', 'some_kwarg2': 'some value2'}},\n            parse_search_terms('kwargs:some_kwarg1=\"some value1\" kwargs:some_kwarg2=\"some value2\"')\n        )\n\n\nclass TestStringfiedDictChecker(unittest.TestCase):\n    def test_stringifies_args(self):\n        self.assertEqual(\n            True,\n            stringified_dict_contains_value('test', 5, \"{'test': 5}\")\n        )\n\n    def test_works_for_no_kwargs(self):\n        self.assertEqual(\n            False,\n            stringified_dict_contains_value('foo', 'bar', None)\n        )\n\n    def test_works_for_nonexisting_kwargs(self):\n        self.assertEqual(\n            False,\n            stringified_dict_contains_value('non_exisiting_kwarg', '5', \"{'test': 5}\")\n        )\n\n    def test_works_for_kwargs_in_different_parts_of_string(self):\n        for key, value in [('key1', '1'), ('key2', '2'), ('key3', '3')]:\n            self.assertEqual(\n                True,\n                stringified_dict_contains_value(key, value, \"{'key1': 1, 'key2': 2, 'key3': 3}\")\n            )\n\n\nclass TestTaskFiltering(unittest.TestCase):\n    def _create_task(self, result=None, args=None, kwargs='{}'):\n        args = args or []\n        TaskMockClass = namedtuple('Task', 'result args kwargs')\n        return TaskMockClass(result, args, kwargs)\n\n    def setUp(self):\n        self.task = self._create_task(\n            args=['arg1'],\n            kwargs=\"{'kwarg1': 1, 'kwarg2': 22, 'kwarg3': '345'}\",\n            result=None,\n        )\n\n    def test_kwarg_search_works(self):\n        self.assertEqual(\n            True,\n            satisfies_search_terms(self.task, dict(kwargs={'kwarg1': 1}))\n        )\n        self.assertEqual(\n            False,\n            satisfies_search_terms(self.task, dict(kwargs={'kwarg1': 2}))\n        )\n        self.assertEqual(\n            False,\n            satisfies_search_terms(self.task, dict(kwargs={'kwarg2': 2}))\n        )\n        self.assertEqual(\n            True,\n            satisfies_search_terms(self.task, dict(kwargs={'kwarg3': '345'}))\n        )\n\n    def test_args_search_works(self):\n        self.assertEqual(\n            True,\n            satisfies_search_terms(self.task, dict(args=['arg1']))\n        )\n        self.assertEqual(\n            False,\n            satisfies_search_terms(self.task, dict(args=['arg2']))\n        )\n        self.assertEqual(\n            False,\n            satisfies_search_terms(self.task, dict(args=['arg']))\n        )\n\n    def test_result_search_handles_none(self):\n        self.assertEqual(\n            False,\n            satisfies_search_terms(self.task, dict(result=['result1']))\n        )\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/unit/utils/test_template.py",
    "content": "import time\nimport unittest\n\nfrom pytz import utc\n\nfrom flower.utils.template import format_time, humanize\n\n\nclass TestHumanize(unittest.TestCase):\n    def test_none(self):\n        self.assertEqual('', humanize(None))\n\n    def test_bool(self):\n        self.assertEqual(True, humanize(True))\n        self.assertEqual(False, humanize(False))\n\n    def test_numbers(self):\n        self.assertEqual(0, humanize(0))\n        self.assertEqual(3, humanize(3))\n        self.assertEqual(0.2, humanize(0.2))\n\n    def test_keywords(self):\n        self.assertEqual('SSL', humanize('ssl'))\n        self.assertEqual('SSL', humanize('SSL'))\n\n        self.assertEqual('URI', humanize('uri'))\n        self.assertEqual('URI', humanize('URI'))\n\n        self.assertEqual('UUID', humanize('uuid'))\n        self.assertEqual('UUID', humanize('UUID'))\n\n        self.assertEqual('ETA', humanize('eta'))\n        self.assertEqual('ETA', humanize('ETA'))\n\n        self.assertEqual('URL', humanize('url'))\n        self.assertEqual('URL', humanize('URL'))\n\n        self.assertEqual('args', humanize('args'))\n        self.assertEqual('kwargs', humanize('kwargs'))\n\n    def test_uuid(self):\n        uuid = '5cf83762-9507-4dc5-8e5a-ad730379b099'\n        self.assertEqual(uuid, humanize(uuid))\n\n    def test_sequences(self):\n        self.assertEqual('2, 3', humanize([2, 3]))\n        self.assertEqual('2, foo, 1.2', humanize([2, 'foo', 1.2]))\n        self.assertEqual([None, None], humanize([None, None]))\n        self.assertEqual([4, {1: 1}], humanize([4, {1: 1}]))\n\n    def test_time(self):\n        self.assertEqual(1343911558.305793, humanize(1343911558.305793))\n        self.assertEqual(format_time(1343911558.305793, utc),\n                         humanize(1343911558.305793, type='time'))\n\n    def test_natural_time(self):\n        self.assertEqual(humanize(time.time()-1, type='natural-time-utc'),\n                         'a second ago')\n        self.assertEqual(humanize(time.time()-1, type='natural-time'),\n                         'a second ago')\n\n    def test_strings(self):\n        self.assertEqual('Max tasks per child',\n                         humanize('max_tasks_per_child'))\n        self.assertEqual('URI prefix', humanize('uri_prefix'))\n        self.assertEqual('Max concurrency', humanize('max-concurrency'))\n\n    def test_truncate(self):\n        self.assertEqual(humanize(\"1234567\", length=6), '12 ...')\n"
  },
  {
    "path": "tests/unit/utils/test_utils.py",
    "content": "import os\nimport tempfile\nimport unittest\nfrom unittest.mock import Mock, patch\n\nfrom celery import Celery\n\nfrom flower.utils import abs_path, bugreport, gen_cookie_secret, strtobool\n\n\nclass BugreportTests(unittest.TestCase):\n    def test_default(self):\n        report = bugreport()\n        self.assertFalse('Error when generating bug report' in report)\n        self.assertTrue('tornado' in report)\n        self.assertTrue('humanize' in report)\n        self.assertTrue('celery' in report)\n\n    def test_with_app(self):\n        app = Celery()\n        report = bugreport(app)\n        self.assertFalse('Error when generating bug report' in report)\n        self.assertTrue('tornado' in report)\n        self.assertTrue('humanize' in report)\n        self.assertTrue('celery' in report)\n\n    def test_when_unable_to_generate_report(self):\n        fake_app = Mock()\n        fake_app.bugreport.side_effect = ImportError('import error message')\n        report = bugreport(fake_app)\n        self.assertTrue('Error when generating bug report' in report)\n        self.assertTrue('import error message' in report)\n        self.assertTrue(\"Have you installed correct versions of Flower's dependencies?\" in report)\n\n\nclass TestGenCookieSecret(unittest.TestCase):\n    def test_cookie_secret_generation(self):\n        secret1 = gen_cookie_secret()\n        secret2 = gen_cookie_secret()\n        self.assertNotEqual(secret1, secret2)\n\n\nclass TestAbsPath(unittest.TestCase):\n    def test_absolute_path(self):\n        self.assertEqual(abs_path('/home/user/file.txt'), '/home/user/file.txt')\n\n    @unittest.skip\n    def test_relative_path(self):\n        with tempfile.TemporaryDirectory() as tmp_dir:\n            original_dir = os.getcwd()\n            try:\n                os.chdir(tmp_dir)\n                path = abs_path('file.txt')\n                expected = os.path.join(tmp_dir, 'file.txt')\n                self.assertEqual(path, expected)\n            finally:\n                os.chdir(original_dir)\n\n    def test_relative_path_with_pwd(self):\n        with patch.dict(os.environ, {'PWD': '/home/user'}):\n            self.assertEqual(abs_path('file.txt'), '/home/user/file.txt')\n\n    def test_home_path(self):\n        self.assertEqual(abs_path('~/file.txt'), os.path.join(os.path.expanduser('~'), 'file.txt'))\n\n\nclass TestStrtobool(unittest.TestCase):\n    def test_true_values(self):\n        self.assertEqual(strtobool('y'), 1)\n        self.assertEqual(strtobool('yes'), 1)\n        self.assertEqual(strtobool('t'), 1)\n        self.assertEqual(strtobool('true'), 1)\n        self.assertEqual(strtobool('on'), 1)\n        self.assertEqual(strtobool('1'), 1)\n\n    def test_false_values(self):\n        self.assertEqual(strtobool('n'), 0)\n        self.assertEqual(strtobool('no'), 0)\n        self.assertEqual(strtobool('f'), 0)\n        self.assertEqual(strtobool('false'), 0)\n        self.assertEqual(strtobool('off'), 0)\n        self.assertEqual(strtobool('0'), 0)\n\n    def test_invalid_value(self):\n        self.assertRaises(ValueError, strtobool, 'invalid')\n"
  },
  {
    "path": "tests/unit/views/__init__.py",
    "content": ""
  },
  {
    "path": "tests/unit/views/test_auth.py",
    "content": "from flower.views.auth import authenticate, validate_auth_option\nfrom tests.unit import AsyncHTTPTestCase\n\n\nclass BasicAuthTests(AsyncHTTPTestCase):\n    def test_with_single_creds(self):\n        with self.mock_option('basic_auth', ['foo:bar']):\n            r = self.fetch('/')\n            self.assertEqual(401, r.code)\n            r = self.fetch('/', auth_username='foo', auth_password='bar')\n            self.assertEqual(200, r.code)\n            r = self.fetch('/', auth_username='foo', auth_password='bar2')\n            self.assertEqual(401, r.code)\n\n    def test_with_multiple_creds(self):\n        with self.mock_option('basic_auth', ['user1:pswd1', 'user2:pswd2']):\n            r = self.fetch('/')\n            self.assertEqual(401, r.code)\n            r = self.fetch('/', auth_username='user1', auth_password='pswd1')\n            self.assertEqual(200, r.code)\n            r = self.fetch('/', auth_username='user2', auth_password='pswd2')\n            self.assertEqual(200, r.code)\n            r = self.fetch('/', auth_username='user1', auth_password='pswd2')\n            self.assertEqual(401, r.code)\n\n\nclass AuthTests(AsyncHTTPTestCase):\n    def test_validate_auth_option(self):\n        self.assertTrue(validate_auth_option(\"mail@example.com\"))\n        self.assertTrue(validate_auth_option(\".*@example.com\"))\n        self.assertTrue(validate_auth_option(\"one.*@example.com\"))\n        self.assertTrue(validate_auth_option(\"one.*two@example.com\"))\n        self.assertFalse(validate_auth_option(\".*@.*example.com\"))\n        self.assertFalse(validate_auth_option(\"one@domain1.com|.*@domain2.com\"))\n        self.assertTrue(validate_auth_option(\"one@example.com|two@example.com\"))\n        self.assertFalse(validate_auth_option(\"mail@.*example.com\"))\n        self.assertFalse(validate_auth_option(\".*example.com\"))\n\n    def test_authenticate_single_email(self):\n        self.assertTrue(authenticate(\"mail@example.com\", \"mail@example.com\"))\n        self.assertFalse(authenticate(\"mail@example.com\", \"foo@example.com\"))\n        self.assertFalse(authenticate(\"mail@example.com\", \"long.mail@example.com\"))\n        self.assertFalse(authenticate(\"mail@example.com\", \"\"))\n        self.assertFalse(authenticate(\"me@gmail.com\", \"me@gmail.com.attacker.com\"))\n        self.assertFalse(authenticate(\"me@gmail.com\", \"*\"))\n\n    def test_authenticate_email_list(self):\n        self.assertTrue(authenticate(\"one@example.com|two@example.net\", \"one@example.com\"))\n        self.assertTrue(authenticate(\"one@example.com|two@example.net\", \"two@example.net\"))\n        self.assertFalse(authenticate(\"one@example.com|two@example.net\", \"two@example.com\"))\n        self.assertFalse(authenticate(\"one@example.com|two@example.net\", \"one@example.net\"))\n        self.assertFalse(authenticate(\"one@example.com|two@example.net\", \"mail@gmail.com\"))\n        self.assertFalse(authenticate(\"one@example.com|two@example.net\", \"\"))\n        self.assertFalse(authenticate(\"one@example.com|two@example.net\", \"*\"))\n\n    def test_authenticate_wildcard_email(self):\n        self.assertTrue(authenticate(\".*@example.com\", \"one@example.com\"))\n        self.assertTrue(authenticate(\"one.*@example.com\", \"one@example.com\"))\n        self.assertTrue(authenticate(\"one.*@example.com\", \"one.two@example.com\"))\n        self.assertFalse(authenticate(\".*@example.com\", \"attacker@example.com.attacker.com\"))\n        self.assertFalse(authenticate(\".*@corp.example.com\", \"attacker@corpZexample.com\"))\n        self.assertFalse(authenticate(\".*@corp\\.example\\.com\", \"attacker@corpZexample.com\"))\n"
  },
  {
    "path": "tests/unit/views/test_broker.py",
    "content": "from tests.unit import AsyncHTTPTestCase\n\n\nclass TestBrokerView(AsyncHTTPTestCase):\n    def test_empty_page(self):\n        res = self.get('/broker')\n        self.assertEqual(200, res.code)\n"
  },
  {
    "path": "tests/unit/views/test_error.py",
    "content": "from tests.unit import AsyncHTTPTestCase\n\n\nclass ErrorTests(AsyncHTTPTestCase):\n    def test_404(self):\n        r = self.get('/unknown')\n        self.assertEqual(404, r.code)\n"
  },
  {
    "path": "tests/unit/views/test_monitor.py",
    "content": "import re\nimport time\nfrom datetime import datetime, timedelta\n\nfrom celery.events import Event\nfrom kombu import uuid\n\nfrom flower.events import EventsState\nfrom tests.unit import AsyncHTTPTestCase\nfrom tests.unit.utils import task_failed_events, task_succeeded_events\n\n\nclass PrometheusTests(AsyncHTTPTestCase):\n    def setUp(self):\n        self.app = super().get_app()\n        super().setUp()\n\n    def get_app(self, capp=None):\n        return self.app\n\n    def test_metrics(self):\n        state = EventsState()\n        worker_name = 'worker1'\n        task_name = 'task1'\n        state.get_or_create_worker(worker_name)\n        events = [\n            Event('worker-online', hostname=worker_name), Event('worker-heartbeat', hostname=worker_name, active=1)\n        ]\n        events += task_succeeded_events(worker=worker_name, name=task_name, id='123')\n\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n        events = dict(re.findall('flower_events_total{task=\"task1\",type=\"(task-.*)\",worker=\"worker1\"} (.*)', metrics))\n\n        self.assertTrue('task-received' in events)\n        self.assertTrue('task-started' in events)\n        self.assertTrue('task-succeeded' in events)\n\n        self.assertTrue(f'flower_worker_online{{worker=\"{worker_name}\"}} 1.0' in metrics)\n        self.assertTrue(f'flower_worker_number_of_currently_executing_tasks{{worker=\"{worker_name}\"}} 1.0' in metrics)\n\n    def test_task_prefetch_time_metric(self):\n        state = EventsState()\n        worker_name = 'worker1'\n        task_name = 'task1'\n        state.get_or_create_worker(worker_name)\n        events = task_succeeded_events(worker=worker_name, name=task_name, id='123')[:-1]\n\n        task_received = time.time()\n        task_started = task_received + 3\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            if e['type'] == 'task-received':\n                e['timestamp'] = task_received\n            if e['type'] == 'task-started':\n                e['timestamp'] = task_started\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertTrue(\n            f'flower_task_prefetch_time_seconds{{task=\"{task_name}\",worker=\"{worker_name}\"}} 3.0' in metrics\n        )\n\n    def test_task_prefetch_time_metric_successful_task_resets_metric_to_zero(self):\n        state = EventsState()\n        worker_name = 'worker1'\n        task_name = 'task1'\n        state.get_or_create_worker(worker_name)\n        events = task_succeeded_events(worker=worker_name, name=task_name, id='123')\n\n        task_received = time.time()\n        task_started = task_received + 3\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            if e['type'] == 'task-received':\n                e['timestamp'] = task_received\n            if e['type'] == 'task-started':\n                e['timestamp'] = task_started\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertTrue(\n            f'flower_task_prefetch_time_seconds{{task=\"{task_name}\",worker=\"{worker_name}\"}} 0.0' in metrics\n        )\n\n    def test_task_prefetch_time_metric_failed_task_resets_metric_to_zero(self):\n        state = EventsState()\n        worker_name = 'worker1'\n        task_name = 'task1'\n        state.get_or_create_worker(worker_name)\n        events = task_failed_events(worker=worker_name, name=task_name, id='123')\n\n        task_received = time.time()\n        task_started = task_received + 3\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            if e['type'] == 'task-received':\n                e['timestamp'] = task_received\n            if e['type'] == 'task-started':\n                e['timestamp'] = task_started\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertTrue(\n            f'flower_task_prefetch_time_seconds{{task=\"{task_name}\",worker=\"{worker_name}\"}} 0.0' in metrics\n        )\n\n    def test_task_prefetch_time_metric_does_not_compute_prefetch_time_if_task_has_eta(self):\n        state = EventsState()\n        worker_name = 'worker2'\n        task_name = 'task2'\n        state.get_or_create_worker(worker_name)\n        events = [Event('worker-online', hostname=worker_name)]\n        events += task_succeeded_events(\n            worker=worker_name, name=task_name, id='567', eta=datetime.now() + timedelta(hours=4)\n        )\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertFalse(\n            f'flower_task_prefetch_time_seconds{{task=\"{task_name}\",worker=\"{worker_name}\"}} ' in metrics\n        )\n\n    def test_worker_online_metric_worker_is_offline(self):\n        state = EventsState()\n        worker_name = 'worker1'\n        state.get_or_create_worker(worker_name)\n        events = [Event('worker-offline', hostname=worker_name)]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertTrue(f'flower_worker_online{{worker=\"{worker_name}\"}} 0.0' in metrics)\n\n    def test_worker_prefetched_tasks_metric(self):\n        state = EventsState()\n        worker_name = 'worker2'\n        task_name = 'task1'\n        task_id = uuid()\n        state.get_or_create_worker(worker_name)\n        events = [\n            Event(\n                'task-received',\n                uuid=task_id,\n                name=task_name,\n                args='(2, 2)',\n                kwargs=\"{'foo': 'bar'}\",\n                retries=1,\n                eta=None,\n                hostname=worker_name\n            ),\n            Event(\n                'task-received',\n                uuid=uuid(),\n                name=task_name,\n                args='(2, 2)',\n                kwargs=\"{'foo': 'bar'}\",\n                retries=1,\n                eta=None,\n                hostname=worker_name\n            ),\n            Event('task-started', uuid=task_id, hostname=worker_name),\n        ]\n\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        metrics = self.get('/metrics').body.decode('utf-8')\n\n        self.assertTrue(\n            f'flower_worker_prefetched_tasks{{task=\"{task_name}\",worker=\"{worker_name}\"}} 1.0' in metrics\n        )\n\n\nclass HealthcheckTests(AsyncHTTPTestCase):\n    def setUp(self):\n        self.app = super().get_app()\n        super().setUp()\n\n    def get_app(self, capp=None):\n        return self.app\n\n    def test_healthcheck_route(self):\n        response = self.get('/healthcheck').body.decode('utf-8')\n        self.assertEqual(response, 'OK')\n"
  },
  {
    "path": "tests/unit/views/test_tasks.py",
    "content": "import json\nimport time\n\nfrom celery.events import Event\n\nfrom flower.events import EventsState\nfrom tests.unit import AsyncHTTPTestCase\nfrom tests.unit.utils import task_failed_events, task_succeeded_events\n\n\nclass TaskTest(AsyncHTTPTestCase):\n    def test_unknown_task(self):\n        r = self.get('/task/unknown')\n        self.assertEqual(404, r.code)\n        self.assertTrue('Unknown task' in str(r.body))\n\n\nclass TasksTest(AsyncHTTPTestCase):\n    def setUp(self):\n        self.app = super().get_app()\n        super().setUp()\n\n    def get_app(self, capp=None):\n        return self.app\n\n    def test_no_task(self):\n        r = self.get('/tasks')\n        self.assertEqual(200, r.code)\n        self.assertTrue('UUID' in str(r.body))\n        self.assertNotIn('<tr id=', str(r.body))\n\n    def test_succeeded_task(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='123')\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        params = dict(draw=1, start=0, length=10)\n        params['search[value]'] = ''\n        params['order[0][column]'] = 0\n        params['columns[0][data]'] = 'name'\n        params['order[0][dir]'] = 'asc'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(1, table['recordsTotal'])\n        self.assertEqual(1, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(1, len(tasks))\n        self.assertEqual('SUCCESS', tasks[0]['state'])\n        self.assertEqual('task1', tasks[0]['name'])\n        self.assertEqual('123', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n\n    def test_failed_task(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_failed_events(worker='worker1', name='task1',\n                                     id='123')\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        params = dict(draw=1, start=0, length=10)\n        params['search[value]'] = ''\n        params['order[0][column]'] = 0\n        params['columns[0][data]'] = 'name'\n        params['order[0][dir]'] = 'asc'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(1, table['recordsTotal'])\n        self.assertEqual(1, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(1, len(tasks))\n        self.assertEqual('FAILURE', tasks[0]['state'])\n        self.assertEqual('task1', tasks[0]['name'])\n        self.assertEqual('123', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n\n    def test_sort_runtime(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='2', runtime=10.0)\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='4', runtime=10000000.0)\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='3', runtime=20.0)\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='1', runtime=2.0)\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        params = dict(draw=1, start=0, length=10)\n        params['search[value]'] = ''\n        params['order[0][column]'] = 0\n        params['columns[0][data]'] = 'runtime'\n        params['order[0][dir]'] = 'asc'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(4, table['recordsTotal'])\n        self.assertEqual(4, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(4, len(tasks))\n\n        self.assertEqual('SUCCESS', tasks[0]['state'])\n        self.assertEqual('task1', tasks[0]['name'])\n        self.assertEqual('1', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n        self.assertEqual(2.0, tasks[0]['runtime'])\n\n        self.assertEqual('SUCCESS', tasks[1]['state'])\n        self.assertEqual('task1', tasks[1]['name'])\n        self.assertEqual('2', tasks[1]['uuid'])\n        self.assertEqual('worker1', tasks[1]['worker'])\n        self.assertEqual(10.0, tasks[1]['runtime'])\n\n        self.assertEqual('SUCCESS', tasks[3]['state'])\n        self.assertEqual('task1', tasks[3]['name'])\n        self.assertEqual('4', tasks[3]['uuid'])\n        self.assertEqual('worker1', tasks[3]['worker'])\n        self.assertEqual(10000000.0, tasks[3]['runtime'])\n\n    def test_sort_incomparable(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='123')\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='456', runtime=None)\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        params = dict(draw=1, start=0, length=10)\n        params['search[value]'] = ''\n        params['order[0][column]'] = 0\n        params['columns[0][data]'] = 'runtime'\n        params['order[0][dir]'] = 'asc'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, table['recordsTotal'])\n        self.assertEqual(2, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(2, len(tasks))\n\n        self.assertEqual('SUCCESS', tasks[0]['state'])\n        self.assertEqual('task1', tasks[0]['name'])\n        self.assertEqual('456', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n        self.assertIsNone(tasks[0]['runtime'])\n\n        self.assertEqual('SUCCESS', tasks[1]['state'])\n        self.assertEqual('task1', tasks[1]['name'])\n        self.assertEqual('123', tasks[1]['uuid'])\n        self.assertEqual('worker1', tasks[1]['worker'])\n\n    def test_pagination(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        events = [Event('worker-online', hostname='worker1')]\n        events += task_succeeded_events(worker='worker1', name='task1',\n                                        id='123')\n        events += task_succeeded_events(worker='worker1', name='task2',\n                                        id='456')\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n        self.app.events.state = state\n\n        params = dict(draw=1, start=0, length=10)\n        params['search[value]'] = ''\n        params['order[0][column]'] = 0\n        params['columns[0][data]'] = 'name'\n        params['order[0][dir]'] = 'asc'\n        params['start'] = '0'\n        params['length'] = '1'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, table['recordsTotal'])\n        self.assertEqual(2, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(1, len(tasks))\n\n        self.assertEqual('SUCCESS', tasks[0]['state'])\n        self.assertEqual('task1', tasks[0]['name'])\n        self.assertEqual('123', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n\n        params['start'] = '1'\n        params['length'] = '1'\n\n        r = self.get('/tasks/datatable?' + '&'.join(\n            map(lambda x: '%s=%s' % x, params.items())))\n\n        table = json.loads(r.body.decode(\"utf-8\"))\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, table['recordsTotal'])\n        self.assertEqual(2, table['recordsFiltered'])\n        tasks = table['data']\n        self.assertEqual(1, len(tasks))\n\n        self.assertEqual('SUCCESS', tasks[0]['state'])\n        self.assertEqual('task2', tasks[0]['name'])\n        self.assertEqual('456', tasks[0]['uuid'])\n        self.assertEqual('worker1', tasks[0]['worker'])\n"
  },
  {
    "path": "tests/unit/views/test_url_handlers.py",
    "content": "import os\nfrom unittest.mock import patch\n\nfrom tornado.web import url\n\nfrom flower.app import rewrite_handler\nfrom tests.unit import AsyncHTTPTestCase\n\n\nclass UrlsTests(AsyncHTTPTestCase):\n    def test_workers_url(self):\n        r = self.get('/workers')\n        self.assertEqual(200, r.code)\n\n    def test_root_url(self):\n        r = self.get('/')\n        self.assertEqual(200, r.code)\n\n    def test_tasks_api_url(self):\n        with patch.dict(os.environ, {\"FLOWER_UNAUTHENTICATED_API\": \"true\"}):\n            r = self.get('/api/tasks')\n            self.assertEqual(200, r.code)\n\n\nclass URLPrefixTests(AsyncHTTPTestCase):\n    def setUp(self):\n        self.url_prefix = '/test_root'\n        with self.mock_option('url_prefix', self.url_prefix):\n            super().setUp()\n\n    def test_tuple_handler_rewrite(self):\n        r = self.get(self.url_prefix + '/workers')\n        self.assertEqual(200, r.code)\n\n    def test_root_url(self):\n        r = self.get(self.url_prefix + '/')\n        self.assertEqual(200, r.code)\n\n    def test_tasks_api_url(self):\n        with patch.dict(os.environ, {'FLOWER_UNAUTHENTICATED_API': 'true'}):\n            r = self.get(self.url_prefix + '/api/tasks')\n            self.assertEqual(200, r.code)\n\n    def test_base_url_no_longer_working(self):\n        r = self.get('/')\n        self.assertEqual(404, r.code)\n\n\nclass RewriteHandlerTests(AsyncHTTPTestCase):\n    def target(self):\n        return None\n\n    def test_url_rewrite_using_URLSpec(self):\n        old_handler = url(r\"/\", self.target, name='test')\n        new_handler = rewrite_handler(old_handler, 'test_root')\n        self.assertIsInstance(new_handler, url)\n        self.assertTrue(new_handler.regex.match('/test_root/'))\n        self.assertFalse(new_handler.regex.match('/'))\n        self.assertFalse(new_handler.regex.match('/'))\n\n    def test_url_rewrite_using_tuple(self):\n        old_handler = (r\"/\", self.target)\n        new_handler = rewrite_handler(old_handler, 'test_root')\n        self.assertIsInstance(new_handler, tuple)\n        self.assertEqual(new_handler[0], '/test_root/')\n"
  },
  {
    "path": "tests/unit/views/test_workers.py",
    "content": "import json\nimport time\nimport unittest\nfrom unittest.mock import patch\n\nfrom celery.events import Event\nfrom celery.utils import uuid\n\nfrom flower.events import EventsState\nfrom tests.unit import AsyncHTTPTestCase\nfrom tests.unit.utils import (HtmlTableParser, task_failed_events,\n                              task_succeeded_events)\n\n\nclass WorkersTests(AsyncHTTPTestCase):\n    def setUp(self):\n        self.app = super().get_app()\n        super().setUp()\n\n    def get_app(self, capp=None):\n        return self.app\n\n    def test_default_page(self):\n        r1 = self.get('/')\n        r2 = self.get('/workers')\n        self.assertEqual(r1.body, r2.body)\n\n    def test_no_workers(self):\n        r = self.get('/workers')\n        self.assertEqual(200, r.code)\n        self.assertIn('Load Average', str(r.body))\n        self.assertNotIn('<tr id=', str(r.body))\n\n    @unittest.skip('disable temporarily')\n    def test_unknown_worker(self):\n        with self.mock_option(\"inspect_timeout\", 1.0):\n            r = self.get('/worker/unknown')\n            self.assertEqual(404, r.code)\n            self.assertIn('Unknown worker', str(r.body))\n\n    def test_single_workers_offline(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        state.event(Event('worker-offline', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n\n        r = self.get('/workers')\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(1, len(table.rows()))\n        self.assertTrue(table.get_row('worker1'))\n        self.assertEqual(['worker1', 'False', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker1'))\n        self.assertFalse(table.get_row('worker2'))\n\n    def test_purge_offline_workers(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        state.event(Event('worker-offline', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n\n        with patch('flower.views.workers.options') as mock_options:\n            mock_options.purge_offline_workers = 0\n            r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(0, len(table.rows()))\n\n    def test_single_workers_online(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(1, len(table.rows()))\n        self.assertTrue(table.get_row('worker1'))\n        self.assertEqual(['worker1', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker1'))\n        self.assertFalse(table.get_row('worker2'))\n\n    def test_task_received(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2'),\n                  Event('task-received', uuid=uuid(), name='task1',\n                        args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                        retries=0, eta=None, hostname='worker1')]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '1', '0', '0', '0', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n\n    def test_task_started(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2'),\n                  Event('task-received', uuid='123', name='task1',\n                        args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                        retries=0, eta=None, hostname='worker1'),\n                  Event('task-started', uuid='123', hostname='worker1')]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '1', '0', '0', '0', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n\n    def test_task_succeeded(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2'),\n                  Event('task-received', uuid='123', name='task1',\n                        args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                        retries=0, eta=None, hostname='worker1'),\n                  Event('task-started', uuid='123', hostname='worker1'),\n                  Event('task-succeeded', uuid='123', result='4',\n                        runtime=0.1234, hostname='worker1')]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '1', '0', '1', '0', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n\n    def test_task_failed(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2'),\n                  Event('task-received', uuid='123', name='task1',\n                        args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                        retries=0, eta=None, hostname='worker1'),\n                  Event('task-started', uuid='123', hostname='worker1'),\n                  Event('task-failed', uuid='123', exception=\"KeyError('foo')\",\n                        traceback='line 1 at main', hostname='worker1')]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '1', '1', '0', '0', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n\n    def test_task_retried(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2'),\n                  Event('task-received', uuid='123', name='task1',\n                        args='(2, 2)', kwargs=\"{'foo': 'bar'}\",\n                        retries=0, eta=None, hostname='worker1'),\n                  Event('task-started', uuid='123', hostname='worker1'),\n                  Event('task-retried', uuid='123', exception=\"KeyError('bar')\",\n                        traceback='line 2 at main', hostname='worker1'),\n                  Event('task-failed', uuid='123', exception=\"KeyError('foo')\",\n                        traceback='line 1 at main', hostname='worker1')]\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(2, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '1', '1', '0', '1', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n\n    def test_tasks(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.get_or_create_worker('worker2')\n        state.get_or_create_worker('worker3')\n        events = [Event('worker-online', hostname='worker1'),\n                  Event('worker-online', hostname='worker2')]\n        for i in range(100):\n            events += task_succeeded_events(worker='worker1')\n        for i in range(10):\n            events += task_succeeded_events(worker='worker3')\n        for i in range(13):\n            events += task_failed_events(worker='worker3')\n        for i, e in enumerate(events):\n            e['clock'] = i\n            e['local_received'] = time.time()\n            state.event(e)\n\n        self.app.events.state = state\n\n        r = self.get('/workers')\n\n        table = HtmlTableParser()\n        table.parse(str(r.body))\n\n        self.assertEqual(200, r.code)\n        self.assertEqual(3, len(table.rows()))\n\n        self.assertEqual(['worker1', 'True', '0', '100', '0', '100', '0', None],\n                         table.get_row('worker1'))\n        self.assertEqual(['worker2', 'True', '0', '0', '0', '0', '0', None],\n                         table.get_row('worker2'))\n        self.assertEqual(['worker3', 'True', '0', '23', '13', '10', '0', None],\n                         table.get_row('worker3'))\n\n    def test_workers_view_json(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n\n        res = self.get('/workers?json=1')\n        self.assertEqual(200, res.code)\n        data = json.loads(res.body)\n        self.assertTrue(\"data\" in data)\n\n    def test_workers_view_refresh(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n\n        with patch.object(self.get_app(), \"update_workers\") as update_workers_mock:\n            res = self.get('/workers?refresh=1')\n            self.assertEqual(200, res.code)\n            update_workers_mock.assert_called()\n\n    def test_workers_page(self):\n        state = EventsState()\n        state.get_or_create_worker('worker1')\n        state.event(Event('worker-online', hostname='worker1',\n                          local_received=time.time()))\n        self.app.events.state = state\n        self.app.inspector.workers['worker1'] = {'registeres': [], 'active_queues': [],\n                                                 'stats': {'total': {'tasks.add': 10, 'tasks.sleep': 1, 'tasks.error': 1},\n                                                           'broker': {'hostname': 'redis', 'userid': None, 'virtual_host': '/', 'port': 6379}}}\n\n        with patch.object(self.get_app(), \"update_workers\") as update_workers_mock:\n            res = self.get('/worker/worker1')\n            self.assertEqual(200, res.code)\n            update_workers_mock.assert_called_once_with(workername='worker1')\n\n        with patch.object(self.get_app(), \"update_workers\") as update_workers_mock:\n            res = self.get('/worker/worker2')\n            self.assertEqual(404, res.code)\n            update_workers_mock.assert_called_once_with(workername='worker2')\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n    # Celery 5.2: only py38–py311 (py312 excluded)\n    {py39,py310,py311}-celery52-{tornado60,tornado61,tornado62,tornado63,tornado64,tornado65},\n    # Celery 5.3: py38–py312\n    {py39,py310,py311,py312}-celery53-{tornado60,tornado61,tornado62,tornado63,tornado64,tornado65},\n    # Celery 5.4: py38–py312\n    {py39,py310,py311,py312}-celery54-{tornado60,tornado61,tornado62,tornado63,tornado64,tornado65},\n    # Celery 5.5: py38–py312\n    {py39,py310,py311,py312}-celery55-{tornado60,tornado61,tornado62,tornado63,tornado64,tornado65},\n    lint\nskip_missing_interpreters = true\n\n[testenv]\ndeps =\n    -r requirements/dev.txt\n    mock\n    celery52: celery==5.2.*\n    celery53: celery==5.3.*\n    celery54: celery==5.4.*\n    celery55: celery==5.5.*\n    tornado60: tornado==6.0.*\n    tornado61: tornado==6.1.*\n    tornado62: tornado==6.2.*\n    tornado63: tornado==6.3.*\n    tornado64: tornado==6.4.*\n    tornado65: tornado==6.5.*\ncommands =\n    python -m flower --version\n    python -m tests.unit\n\n[testenv:lint]\ndeps = pylint\ncommands = pylint flower --rcfile .pylintrc\n"
  }
]