pax_global_header00006660000000000000000000000064147117040030014507gustar00rootroot0000000000000052 comment=ccd760a69f94d6aea2129d8c637e4b3d9843be7e django-q2-1.7.4/000077500000000000000000000000001471170400300133025ustar00rootroot00000000000000django-q2-1.7.4/.git-blame-ignore-revs000066400000000000000000000002131471170400300173760ustar00rootroot00000000000000# flake8, black, isort b1d000d007f3f77069719523268a0c6256dc0860 # move to ruff formatting/linting ad4d24e17c9424b17cd8ae65c2def7ecc74e63c1 django-q2-1.7.4/.github/000077500000000000000000000000001471170400300146425ustar00rootroot00000000000000django-q2-1.7.4/.github/workflows/000077500000000000000000000000001471170400300166775ustar00rootroot00000000000000django-q2-1.7.4/.github/workflows/codeql-analysis.yml000066400000000000000000000046171471170400300225220ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '32 0 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 django-q2-1.7.4/.github/workflows/release.yml000066400000000000000000000014631471170400300210460ustar00rootroot00000000000000name: Release on: push: tags: - '*' jobs: build: if: github.repository == 'django-q2/django-q2' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.11 - name: Install dependencies run: | sudo apt-get update sudo apt-get -y install gettext python -m pip install pip setuptools django poetry # compile messages to get .mo files django-admin compilemessages - name: Build and publish package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') run: poetry --build --username=__token__ --password=${{ secrets.PYPI_TOKEN }} publish django-q2-1.7.4/.github/workflows/test.yml000066400000000000000000000060531471170400300204050ustar00rootroot00000000000000name: tests on: push: branches: - master pull_request: branches: - master jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Lint with ruff run: | pipx install ruff==0.4.10 ruff format . --check && ruff check . - name: Check twine run: | python -m pip install twine poetry rstcheck poetry build rstcheck README.rst twine check dist/* test: runs-on: ubuntu-latest strategy: matrix: python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] django: [ "4.2", "5.0", "5.1"] exclude: # django 5.1 does not support 3.8 and 3.9 - python-version: "3.8" django: "5.1" - python-version: "3.9" django: "5.1" # django 5.0 does not support 3.8 and 3.9 - python-version: "3.8" django: "5.0" - python-version: "3.9" django: "5.0" services: disque: image: efrecon/disque:1.0-rc1 ports: - '7711:7711/tcp' mongodb: image: mongo ports: - 27017:27017 postgres: image: postgres env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 # needed because the postgres container does not provide a health check options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis ports: - 6379:6379 options: --entrypoint redis-server steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies with Django ${{ matrix.django }} run: | python -m pip install --upgrade pip pip install poetry poetry add "django==${{ matrix.django }}" --python=${{ matrix.python-version }} poetry install -E testing - name: Run Tests run: | poetry run pytest --cov=./django_q --cov-report=xml env: MONGO_HOST: "127.0.0.1" REDIS_HOST: "127.0.0.1" - name: Upload to coveralls run: | python -m pip install coveralls coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-django-${{ matrix.django }} COVERALLS_PARALLEL: true finish: needs: test runs-on: ubuntu-latest container: python:3.11-bookworm steps: - name: Upload to coveralls run: | python -m pip install --upgrade pip python -m pip install coveralls coveralls --service=github --finish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} django-q2-1.7.4/.gitignore000066400000000000000000000015221471170400300152720ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .DS_Store # Translations *.pot *.mo # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ dev-requirements.txt dev-requirements.in manage.py db.sqlite3 *.ipynb *.rdb .venv .env .idea djq node_modules /c.cache/ /dq /venv/ .localdjango-q2-1.7.4/.readthedocs.yaml000066400000000000000000000006701471170400300165340ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 tools: python: "3.9" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py formats: all python: install: - requirements: docs/requirements.txt django-q2-1.7.4/CHANGELOG.md000066400000000000000000002034311471170400300151160ustar00rootroot00000000000000# Changelog ## [v1.7.4](https://github.com/django-q2/django-q2/tree/v1.7.4) (2024-11-03) - Decrease the MAX_RSS set in test_cluster::test_max_rss https://github.com/django-q2/django-q2/pull/240 - Fix BROKER_CLASS monkeypatch in test_brokers https://github.com/django-q2/django-q2/pull/239 - Fix 'receive_message_wait_time_seconds' SQS broker management https://github.com/django-q2/django-q2/pull/243 ## [v1.7.3](https://github.com/django-q2/django-q2/tree/v1.7.3) (2024-10-15) - Catch missing SEGALRM with AttributeError instead of ValueError https://github.com/django-q2/django-q2/pull/223 - Refactor timeout handling to handle AttributeError and ValueError for Windows users https://github.com/django-q2/django-q2/pull/234 - Fix type check for args in scheduler.py E721 https://github.com/django-q2/django-q2/pull/233 - Only trigger prometheus if configured https://github.com/django-q2/django-q2/pull/231 - Fix missing ack_id when finishing task https://github.com/django-q2/django-q2/pull/224 ## [v1.7.2](https://github.com/django-q2/django-q2/tree/v1.7.2) (2024-09-09) - Fix twine check ## [v1.7.1](https://github.com/django-q2/django-q2/tree/v1.7.1) (2024-09-08) - Fixed date of v1.7.0 - Fixed README.rst formatting which is blocking release of latest version ## [v1.7.0](https://github.com/django-q2/django-q2/tree/v1.7.0) (2024-09-08) **Merged pull requests:** - Remove support for Django 3.2 and 4.1 https://github.com/django-q2/django-q2/pull/183 - Fix max attempts for value 1 https://github.com/django-q2/django-q2/pull/185 - Replace black/isort with ruff https://github.com/django-q2/django-q2/pull/188 - Fix repeating task after timeout https://github.com/django-q2/django-q2/pull/184 - fix: Oracle ORM backend compatibility #180 https://github.com/django-q2/django-q2/pull/186 - chore: Update CI for Django 4.2 Python 3.12 support https://github.com/django-q2/django-q2/pull/208 - chore: Add Support Django 5.1 https://github.com/django-q2/django-q2/pull/207 - Call mark_process_dead on worker pid if prometheus_client is installed https://github.com/django-q2/django-q2/pull/212 - Add example project https://github.com/django-q2/django-q2/pull/215 - Add index on succeeded tasks https://github.com/django-q2/django-q2/pull/164 ## [v1.6.2](https://github.com/django-q2/django-q2/tree/v1.6.2) (2024-03-05) **Merged pull requests:** - Allow different broker on chain https://github.com/django-q2/django-q2/pull/156 - Fix formatting issues in README.rst https://github.com/django-q2/django-q2/pull/159 - Update docs to add cluster option to the async_task https://github.com/django-q2/django-q2/pull/157 - Update release/test dependencies https://github.com/django-q2/django-q2/pull/147 - Fix for Negative Repeat Count in Scheduler https://github.com/django-q2/django-q2/pull/146 - Update django.po https://github.com/django-q2/django-q2/pull/138 - Use importerror for b62_decode and avoid deprecation notification https://github.com/django-q2/django-q2/pull/134 - Specify build system in pyproject.toml https://github.com/django-q2/django-q2/pull/131 ## [v1.6.1](https://github.com/django-q2/django-q2/tree/v1.6.1) (2023-10-13) **Merged pull requests:** - Fix strict versions for python/django https://github.com/django-q2/django-q2/pull/130 ## [v1.6.0](https://github.com/django-q2/django-q2/tree/v1.6.0) (2023-10-12) **Merged pull requests:** - Add support for Django 5 and python 12 https://github.com/django-q2/django-q2/pull/120 - Fix for "apps not ready" in Windows and Mac https://github.com/django-q2/django-q2/pull/116 - Update broken MongoClient link in Docs https://github.com/django-q2/django-q2/pull/127 - Fix German Translation Typo https://github.com/django-q2/django-q2/pull/124 - Update Add-ons install command in install.rst https://github.com/django-q2/django-q2/pull/115 - DOCS: Correct health check import in examples.rst https://github.com/django-q2/django-q2/pull/110 ## [v1.5.5](https://github.com/django-q2/django-q2/tree/v1.5.5) (2023-09-01) **Merged pull requests:** - Add documentation to migrate from django-q to django-q2 https://github.com/django-q2/django-q2/pull/108 - Fix not picking up result from falsy result https://github.com/django-q2/django-q2/pull/107 - Remove deprecated usage pkg_resources https://github.com/django-q2/django-q2/pull/103 - Move worker, scheduler, pusher and monitor to separate files https://github.com/django-q2/django-q2/pull/100 ## [v1.5.4](https://github.com/GDay/django-q2/tree/v1.5.4) (2023-06-29) **Merged pull requests:** - Rerun successful tasks https://github.com/django-q2/django-q2/pull/99 ## [v1.5.3](https://github.com/GDay/django-q2/tree/v1.5.3) (2023-05-14) **Merged pull requests:** - Add post_spawn signal. https://github.com/django-q2/django-q2/pull/93 - Post spawn docs https://github.com/django-q2/django-q2/pull/95 - Make processes identifiable with uuid4 https://github.com/django-q2/django-q2/pull/91 ## [v1.5.2](https://github.com/GDay/django-q2/tree/v1.5.2) (2023-04-13) **Merged pull requests:** - Added Django 4.2 to the test matrix, fixed deprecation warning https://github.com/GDay/django-q2/pull/89 - Updated docs to show support for 4.2 ## [v1.5.1](https://github.com/GDay/django-q2/tree/v1.5.1) (2023-04-02) - Fix release to pipy due to changed org name ## [v1.5.0](https://github.com/GDay/django-q2/tree/v1.5.0) (2023-04-02) **Merged pull requests:** - Multiple queue, multiple cluster in one site https://github.com/GDay/django-q2/pull/71 - Allow building docs all formats https://github.com/GDay/django-q2/pull/77 - Remove version locking on `poetry_core` to fix regex error https://github.com/GDay/django-q2/pull/87 - Update dependencies (2023-04-02) https://github.com/GDay/django-q2/pull/88 ## [v1.4.11](https://github.com/GDay/django-q2/tree/v1.4.11) (2023-01-30) **Merged pull requests:** - Fix missing setup file for "No matching distribution" error https://github.com/GDay/django-q2/pull/69 - Remove custom build (revert to auto create setup file) https://github.com/GDay/django-q2/pull/70 ## [v1.4.10](https://github.com/GDay/django-q2/tree/v1.4.10) (2023-01-26) **Merged pull requests:** - Adding translation mo files automatically on build https://github.com/GDay/django-q2/pull/65 - Update all dependencies https://github.com/GDay/django-q2/pull/64 - Bump translations to latest changes https://github.com/GDay/django-q2/pull/63 - Add meaningfull process titles with currently running task name https://github.com/GDay/django-q2/pull/57 - Fix use of database router for write queries and remove Conf.HAS_REPLICA https://github.com/GDay/django-q2/pull/61 - Add intended_date_kwarg field to Schedule https://github.com/GDay/django-q2/pull/62 - Change task timeout logic to have now() as execution time https://github.com/GDay/django-q2/pull/58 - More explicit log messages in exception handling https://github.com/GDay/django-q2/pull/59 ## [v1.4.9](https://github.com/GDay/django-q2/tree/v1.4.9) (2022-12-22) **Merged pull requests:** - Fix DST timezone change (move from DST to normal jump) https://github.com/GDay/django-q2/pull/56 ## [v1.4.8](https://github.com/GDay/django-q2/tree/v1.4.8) (2022-12-21) **Merged pull requests:** - Fix: allow both ZoneInfo and Pytz depending on django version https://github.com/GDay/django-q2/pull/55 ## [v1.4.7](https://github.com/GDay/django-q2/tree/v1.4.7) (2022-12-21) **Merged pull requests:** - Fix: handling exceptions inside job function https://github.com/GDay/django-q2/pull/51 - Fix: Daylight saving time issue with scheduler https://github.com/GDay/django-q2/pull/47 - Chore: Fix badge and add download badge https://github.com/GDay/django-q2/pull/52 - Chore: Remove release drafter https://github.com/GDay/django-q2/pull/53 ## [v1.4.6](https://github.com/GDay/django-q2/tree/v1.4.6) (2022-11-30) **Merged pull requests:** - Fix: Log exceptions with logger.exception https://github.com/GDay/django-q2/pull/42 - Chore: flake8, isort, black https://github.com/GDay/django-q2/pull/40 ## [v1.4.5](https://github.com/GDay/django-q2/tree/v1.4.5) (2022-11-13) - Fix release workflow ## [v1.4.4](https://github.com/GDay/django-q2/tree/v1.4.4) (2022-11-13) **Merged pull requests:** - Fix: Deprecation warning for Django 5.x https://github.com/GDay/django-q2/pull/34 - Feat: Add biweekly and bimonthly https://github.com/GDay/django-q2/pull/36 - Fix: Fix all translation strings and remove compiled https://github.com/GDay/django-q2/pull/36 ## [v1.4.3](https://github.com/GDay/django-q2/tree/v1.4.3) (2022-11-07) **Merged pull requests:** - Fix: func reference in admin https://github.com/GDay/django-q2/pull/28 - Add python 3.11 support and remove 3.7 (as it was never supported by this package anyway) ## [v1.4.2](https://github.com/GDay/django-q2/tree/v1.4.2) (2022-10-22) **Merged pull requests:** - Make redis dependency optional and update boto3 #22 ## [v1.4.1](https://github.com/GDay/django-q2/tree/v1.4.1) (2022-10-21) **Merged pull requests:** - Fix typo configure.rst https://github.com/GDay/django-q2/pull/1 - Update dependencies https://github.com/GDay/django-q2/pull/2 - Show function name in log when running task https://github.com/GDay/django-q2/pull/3 - Codecov -> Coveralls https://github.com/GDay/django-q2/pull/4 - Fix: readthedocs config file https://github.com/GDay/django-q2/pull/5 - Docs updates https://github.com/GDay/django-q2/pull/6 - Feat: Release plan to pypi https://github.com/GDay/django-q2/pull/7 - Feat: Turkish translations https://github.com/GDay/django-q2/pull/8 - Fix: Connection issues with CONN_MAX_AGE > 0 https://github.com/GDay/django-q2/pull/9 - Replace use of eval() by ast.parse() + ast.literal_eval() https://github.com/GDay/django-q2/pull/10 - Admin improvements https://github.com/GDay/django-q2/pull/11 - Save limit per group/func/name https://github.com/GDay/django-q2/pull/12 - Use logger.hasHandlers() to setup fallback logging https://github.com/GDay/django-q2/pull/13 - allow atomic on external db https://github.com/GDay/django-q2/pull/14 - Fix install command and remove old warnings https://github.com/GDay/django-q2/pull/16 - Remove arrow dependency https://github.com/GDay/django-q2/pull/17 - Remove funding file, add docker/compose for development project and fix https://github.com/GDay/django-q2/pull/18 - Fix unclear error when function is not called correctly https://github.com/GDay/django-q2/pull/19 - Remove blessed dependency https://github.com/GDay/django-q2/pull/20 - Release new version and docs fixes https://github.com/GDay/django-q2/pull/21 ## v1.4.0 **Closed issues:** - Upgrade to 1.3.6 stops the cluster from working [\#565](https://github.com/Koed00/django-q/issues/565) - Feature request: override retry for individual tasks [\#551](https://github.com/Koed00/django-q/issues/551) **Merged pull requests:** - Post execute signal and tests [\#580](https://github.com/Koed00/django-q/pull/580) ([Koed00](https://github.com/Koed00)) ## [v1.3.9](https://github.com/koed00/django-q/tree/v1.3.9) (2021-06-10) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.8...v1.3.9) **Closed issues:** - Version 1.3.7 rolled back Arrow to old 0.15.6 [\#571](https://github.com/Koed00/django-q/issues/571) - Migration XYZ dependencies reference nonexistent parent node \('django\_q', '0014\_auto\_20210502\_1221'\) [\#570](https://github.com/Koed00/django-q/issues/570) **Merged pull requests:** - Autofield [\#574](https://github.com/Koed00/django-q/pull/574) ([Koed00](https://github.com/Koed00)) - Fix RemovedInDjango41Warning [\#572](https://github.com/Koed00/django-q/pull/572) ([nurettin](https://github.com/nurettin)) ## [v1.3.8](https://github.com/koed00/django-q/tree/v1.3.8) (2021-06-08) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.7...v1.3.8) ## [v1.3.7](https://github.com/koed00/django-q/tree/v1.3.7) (2021-06-03) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.6...v1.3.7) **Closed issues:** - Q configured to use redis but keeps a continuous connection to db [\#559](https://github.com/Koed00/django-q/issues/559) - \[Error\] select\_for\_update happening when using replica\(read-only\) and default\(write-only\) DB. [\#558](https://github.com/Koed00/django-q/issues/558) **Merged pull requests:** - Build improvements [\#569](https://github.com/Koed00/django-q/pull/569) ([Koed00](https://github.com/Koed00)) - Create codeql-analysis.yml [\#564](https://github.com/Koed00/django-q/pull/564) ([Koed00](https://github.com/Koed00)) - Fix docs error [\#563](https://github.com/Koed00/django-q/pull/563) ([aken830806](https://github.com/aken830806)) - Codecov\_fixes [\#562](https://github.com/Koed00/django-q/pull/562) ([Koed00](https://github.com/Koed00)) - Feature/improves multiple databases support [\#561](https://github.com/Koed00/django-q/pull/561) ([abxsantos](https://github.com/abxsantos)) ## [v1.3.6](https://github.com/koed00/django-q/tree/v1.3.6) (2021-05-14) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.5...v1.3.6) **Closed issues:** - Chain of Task [\#547](https://github.com/Koed00/django-q/issues/547) - Queued tasks \(OrmQ\) are not always acknowledged [\#545](https://github.com/Koed00/django-q/issues/545) - Auto-created primary key used when not defining a primary key type [\#543](https://github.com/Koed00/django-q/issues/543) - Tasks are "processing" but hooks are not called and cluster don't recgonize they are finished and "processed" [\#540](https://github.com/Koed00/django-q/issues/540) - Django Q is still a young project. [\#528](https://github.com/Koed00/django-q/issues/528) - SSL errors after upgrading to qcluster version 1.1.0 [\#422](https://github.com/Koed00/django-q/issues/422) - \[Enhancement\] Add Systemd sample template [\#420](https://github.com/Koed00/django-q/issues/420) - Not enough values to unpack \(expected 2, got 1\) [\#314](https://github.com/Koed00/django-q/issues/314) - Successful tasks grow beyond save\_limit [\#225](https://github.com/Koed00/django-q/issues/225) **Merged pull requests:** - Fix for SSL errors in \#422 [\#556](https://github.com/Koed00/django-q/pull/556) ([nittolese](https://github.com/nittolese)) - Allow tasks to be scheduled on a specific cluster [\#555](https://github.com/Koed00/django-q/pull/555) ([midse](https://github.com/midse)) - Fixes \#314 - Convert func to its import path str so that resubmitting failed task works [\#554](https://github.com/Koed00/django-q/pull/554) ([kennyhei](https://github.com/kennyhei)) - Add "qmemory" command [\#553](https://github.com/Koed00/django-q/pull/553) ([kennyhei](https://github.com/kennyhei)) - Fixes \#225 - Successful tasks grow beyond SAVE\_LIMIT [\#552](https://github.com/Koed00/django-q/pull/552) ([kennyhei](https://github.com/kennyhei)) - Fixes deprecated count method [\#549](https://github.com/Koed00/django-q/pull/549) ([Koed00](https://github.com/Koed00)) - Updates testing to python 3.9 [\#548](https://github.com/Koed00/django-q/pull/548) ([Koed00](https://github.com/Koed00)) - Update documentation for new retry time default [\#538](https://github.com/Koed00/django-q/pull/538) ([amo13](https://github.com/amo13)) - Use 'timezone.localtime\(\)' when calculating the next run time [\#520](https://github.com/Koed00/django-q/pull/520) ([wy-z](https://github.com/wy-z)) - added long polling support [\#506](https://github.com/Koed00/django-q/pull/506) ([Javedgouri](https://github.com/Javedgouri)) ## [v1.3.5](https://github.com/koed00/django-q/tree/v1.3.5) (2021-02-26) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.4...v1.3.5) **Closed issues:** - Tasks piling up on a slower machine while the fast one has no tasks queued. Help. [\#500](https://github.com/Koed00/django-q/issues/500) - SQL errors when running tasks and viewing successful/failed tasks [\#496](https://github.com/Koed00/django-q/issues/496) - Prevent retry in case of failure \(max\_attempts\) [\#495](https://github.com/Koed00/django-q/issues/495) - How to use with test cases? Scheduled tasks are not being queued when running `python manage.py test`. [\#490](https://github.com/Koed00/django-q/issues/490) - Question: Running django-q in the background. [\#487](https://github.com/Koed00/django-q/issues/487) **Merged pull requests:** - Add a warning for misconfiguration. [\#509](https://github.com/Koed00/django-q/pull/509) ([icfly2](https://github.com/icfly2)) - Migrate to Github Action CI [\#507](https://github.com/Koed00/django-q/pull/507) ([Koed00](https://github.com/Koed00)) - Add example of http health check [\#504](https://github.com/Koed00/django-q/pull/504) ([pysean3](https://github.com/pysean3)) - Add broker name in Schedule and enhanced Queued Tasks list display admin [\#502](https://github.com/Koed00/django-q/pull/502) ([telmobarros](https://github.com/telmobarros)) - Added german translation [\#499](https://github.com/Koed00/django-q/pull/499) ([jonaswinkler](https://github.com/jonaswinkler)) - Update brokers.rst [\#497](https://github.com/Koed00/django-q/pull/497) ([MaximilianKindshofer](https://github.com/MaximilianKindshofer)) ## [v1.3.4](https://github.com/koed00/django-q/tree/v1.3.4) (2020-10-26) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.3...v1.3.4) **Closed issues:** - Admin integration is broken when using ORM as a broker with a different database [\#472](https://github.com/Koed00/django-q/issues/472) - TypeError: can't pickle \_thread.lock objects [\#424](https://github.com/Koed00/django-q/issues/424) **Merged pull requests:** - Fix deprecation warning RemovedInDjango40Warning [\#483](https://github.com/Koed00/django-q/pull/483) ([Djailla](https://github.com/Djailla)) - Fix for \#424 [\#482](https://github.com/Koed00/django-q/pull/482) ([ihuk](https://github.com/ihuk)) - \[WIP\]Change Django documentation links and URLs to a supported version \(v1.8 -\> v2.2\) [\#481](https://github.com/Koed00/django-q/pull/481) ([jagu2012](https://github.com/jagu2012)) - Model.\_\_unicode\_\_\(\) has no effect in Python 3.X [\#479](https://github.com/Koed00/django-q/pull/479) ([alx-sdv](https://github.com/alx-sdv)) - try to get SQS queue before creating it [\#478](https://github.com/Koed00/django-q/pull/478) ([fallenhitokiri](https://github.com/fallenhitokiri)) - empty dictionary as configuration value for SQS [\#477](https://github.com/Koed00/django-q/pull/477) ([fallenhitokiri](https://github.com/fallenhitokiri)) ## [v1.3.3](https://github.com/koed00/django-q/tree/v1.3.3) (2020-08-16) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.2...v1.3.3) **Closed issues:** - Call Django Class based view function with Django-Q's async\_task [\#463](https://github.com/Koed00/django-q/issues/463) - qcluster timezone [\#459](https://github.com/Koed00/django-q/issues/459) - Django-q from other django project in script [\#443](https://github.com/Koed00/django-q/issues/443) **Merged pull requests:** - Add attempt\_count to limit the number of times a filed task will be re-attempted [\#466](https://github.com/Koed00/django-q/pull/466) ([timomeara](https://github.com/timomeara)) - Updates to Django 3.1 [\#464](https://github.com/Koed00/django-q/pull/464) ([Koed00](https://github.com/Koed00)) ## [v1.3.2](https://github.com/koed00/django-q/tree/v1.3.2) (2020-07-08) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.1...v1.3.2) **Closed issues:** - Is it the maintainers' intent to support RabbitMQ in the future? [\#454](https://github.com/Koed00/django-q/issues/454) - Can worker/cluster resource usage be limited? [\#453](https://github.com/Koed00/django-q/issues/453) **Merged pull requests:** - Resource limits [\#457](https://github.com/Koed00/django-q/pull/457) ([Koed00](https://github.com/Koed00)) ## [v1.3.1](https://github.com/koed00/django-q/tree/v1.3.1) (2020-07-02) [Full Changelog](https://github.com/koed00/django-q/compare/v1.3.0...v1.3.1) **Closed issues:** - Ability to customize schedule creation [\#451](https://github.com/Koed00/django-q/issues/451) ## [v1.3.0](https://github.com/koed00/django-q/tree/v1.3.0) (2020-07-02) [Full Changelog](https://github.com/koed00/django-q/compare/v1.2.4...v1.3.0) **Closed issues:** - ERROR: django-picklefield 3.0.1 has requirement Django\>=2.2, but you'll have django 1.11.10 which is incompatible. [\#445](https://github.com/Koed00/django-q/issues/445) - \[Error\] select\_for\_update cannot be used outside of a transaction. [\#434](https://github.com/Koed00/django-q/issues/434) **Merged pull requests:** - Support for Cron expressions [\#452](https://github.com/Koed00/django-q/pull/452) ([Koed00](https://github.com/Koed00)) - Updates packages [\#450](https://github.com/Koed00/django-q/pull/450) ([Koed00](https://github.com/Koed00)) - Adds hint, some linting and a release drafter [\#449](https://github.com/Koed00/django-q/pull/449) ([Koed00](https://github.com/Koed00)) - Use 'force\_str' instead of deprecated 'force\_text' [\#448](https://github.com/Koed00/django-q/pull/448) ([edthrn](https://github.com/edthrn)) - \[cleanup\] Few cleanup commit for linting and migrations [\#447](https://github.com/Koed00/django-q/pull/447) ([Djailla](https://github.com/Djailla)) ## [v1.2.4](https://github.com/koed00/django-q/tree/v1.2.4) (2020-06-10) [Full Changelog](https://github.com/koed00/django-q/compare/v1.2.3...v1.2.4) **Merged pull requests:** - Add missing migration [\#446](https://github.com/Koed00/django-q/pull/446) ([Djailla](https://github.com/Djailla)) ## [v1.2.3](https://github.com/koed00/django-q/tree/v1.2.3) (2020-05-31) [Full Changelog](https://github.com/koed00/django-q/compare/v1.2.2...v1.2.3) ## [v1.2.2](https://github.com/koed00/django-q/tree/v1.2.2) (2020-05-31) [Full Changelog](https://github.com/koed00/django-q/compare/v1.2.1...v1.2.2) **Closed issues:** - Scheduled task being executed many times [\#426](https://github.com/Koed00/django-q/issues/426) - schedule doesn't work [\#416](https://github.com/Koed00/django-q/issues/416) - Expose list of workers and their states via API [\#364](https://github.com/Koed00/django-q/issues/364) - Tasks are not encrypted, only signed [\#300](https://github.com/Koed00/django-q/issues/300) **Merged pull requests:** - Poetry [\#442](https://github.com/Koed00/django-q/pull/442) ([Koed00](https://github.com/Koed00)) - Fix issues when using multiple databases with a database router [\#440](https://github.com/Koed00/django-q/pull/440) ([maerteijn](https://github.com/maerteijn)) - Update documentation to say tasks are signed, not encrypted [\#429](https://github.com/Koed00/django-q/pull/429) ([asedeno](https://github.com/asedeno)) - Fix issue when using USE\_TZ=False with MySQL [\#428](https://github.com/Koed00/django-q/pull/428) ([hhyo](https://github.com/hhyo)) - When sync=True, re-raise exceptions from the worker. [\#417](https://github.com/Koed00/django-q/pull/417) ([rbranche](https://github.com/rbranche)) ## [v1.2.1](https://github.com/koed00/django-q/tree/v1.2.1) (2020-02-18) [Full Changelog](https://github.com/koed00/django-q/compare/v1.2.0...v1.2.1) **Merged pull requests:** - Convert to f-strings [\#415](https://github.com/Koed00/django-q/pull/415) ([Koed00](https://github.com/Koed00)) ## [v1.2.0](https://github.com/koed00/django-q/tree/v1.2.0) (2020-02-17) [Full Changelog](https://github.com/koed00/django-q/compare/v.1.1.0...v1.2.0) **Closed issues:** - Run task at a specific time [\#407](https://github.com/Koed00/django-q/issues/407) - Question about Multiple Clusters [\#401](https://github.com/Koed00/django-q/issues/401) **Merged pull requests:** - Differentiate between PID and unique cluster ID [\#414](https://github.com/Koed00/django-q/pull/414) ([jmcvetta](https://github.com/jmcvetta)) - Bump django from 3.0.2 to 3.0.3 [\#411](https://github.com/Koed00/django-q/pull/411) ([dependabot[bot]](https://github.com/apps/dependabot)) - Fix startup error in AWS Lambda [\#405](https://github.com/Koed00/django-q/pull/405) ([lordkev](https://github.com/lordkev)) - \[py27\] Remove last traces of py27 [\#392](https://github.com/Koed00/django-q/pull/392) ([Djailla](https://github.com/Djailla)) ## [v.1.1.0](https://github.com/koed00/django-q/tree/v.1.1.0) (2020-01-18) [Full Changelog](https://github.com/koed00/django-q/compare/v1.0.2...v.1.1.0) **Closed issues:** - Ability to use a Redis URI [\#402](https://github.com/Koed00/django-q/issues/402) - worker\_1 | 16:15:08 \[Q\] ERROR malformed node or string: \<\_ast.Name object at 0x7fb952093130\> [\#400](https://github.com/Koed00/django-q/issues/400) - Latest versions of Arrow will break django-q [\#377](https://github.com/Koed00/django-q/issues/377) - How to deal with failed tasks? [\#365](https://github.com/Koed00/django-q/issues/365) - Timeout override is lost when sent to broker [\#332](https://github.com/Koed00/django-q/issues/332) - "InterfaceError: connection already closed" being raised when a test is run [\#326](https://github.com/Koed00/django-q/issues/326) - scheduler creating duplicate tasks in multiple cluster environment [\#231](https://github.com/Koed00/django-q/issues/231) **Merged pull requests:** - Django 3 support [\#404](https://github.com/Koed00/django-q/pull/404) ([Koed00](https://github.com/Koed00)) - ability to use a Redis connection URI - closes \#402 [\#403](https://github.com/Koed00/django-q/pull/403) ([valentinogagliardi](https://github.com/valentinogagliardi)) - Replacing `ugettext_` functions with `gettext_` for Django 3 [\#399](https://github.com/Koed00/django-q/pull/399) ([theunraveler](https://github.com/theunraveler)) - Bump django from 2.2.5 to 2.2.8 [\#395](https://github.com/Koed00/django-q/pull/395) ([dependabot[bot]](https://github.com/apps/dependabot)) - Preserve database connection when sync=True [\#393](https://github.com/Koed00/django-q/pull/393) ([Urth](https://github.com/Urth)) - Fix scheduler concurrency with multiple clusters [\#347](https://github.com/Koed00/django-q/pull/347) ([maerteijn](https://github.com/maerteijn)) - Fix timeout override [\#333](https://github.com/Koed00/django-q/pull/333) ([tremby](https://github.com/tremby)) ## [v1.0.2](https://github.com/koed00/django-q/tree/v1.0.2) (2019-08-10) [Full Changelog](https://github.com/koed00/django-q/compare/v1.0.1...v1.0.2) **Closed issues:** - Is django-q dead? [\#375](https://github.com/Koed00/django-q/issues/375) - Cluster shuts down immediately after processing tasks [\#367](https://github.com/Koed00/django-q/issues/367) - Why is django\_q hitting redis so hard? [\#359](https://github.com/Koed00/django-q/issues/359) - ERROR MySQL backend does not support timezone-aware datetimes when USE\_TZ is False. [\#350](https://github.com/Koed00/django-q/issues/350) - from django\_q.tasks import async - ImportError: cannot import name 'async' [\#346](https://github.com/Koed00/django-q/issues/346) - Timeouts given for async\_task do not work if timeout value for cluster is None \(the default\) [\#335](https://github.com/Koed00/django-q/issues/335) - Import Error running qcluster command Python 3.7 Django 2.1.5 [\#331](https://github.com/Koed00/django-q/issues/331) - Periodic tasks add only if [\#320](https://github.com/Koed00/django-q/issues/320) - async\_task not in docs?!? [\#317](https://github.com/Koed00/django-q/issues/317) - django\_q 1.0 fails in ./manage.py check on python 3.4 django 2.0.8 [\#315](https://github.com/Koed00/django-q/issues/315) - Long-running tasks are duplicated multiple times in multi-cluster environment when no timeout is set [\#307](https://github.com/Koed00/django-q/issues/307) - log name used to configure logging in django [\#268](https://github.com/Koed00/django-q/issues/268) - RFC - Interval for schedules [\#265](https://github.com/Koed00/django-q/issues/265) - The error traceback [\#259](https://github.com/Koed00/django-q/issues/259) - Mention Django Q Email in the docs? [\#215](https://github.com/Koed00/django-q/issues/215) **Merged pull requests:** - Remove and re-add task.id field instead of alter [\#363](https://github.com/Koed00/django-q/pull/363) ([wgordon17](https://github.com/wgordon17)) - Fix test section format in readme [\#358](https://github.com/Koed00/django-q/pull/358) ([vkaracic](https://github.com/vkaracic)) - Inline import to prevent circular imports under some toolchain combinations [\#356](https://github.com/Koed00/django-q/pull/356) ([lamby](https://github.com/lamby)) - fix spelling of careful [\#355](https://github.com/Koed00/django-q/pull/355) ([tylerharper](https://github.com/tylerharper)) - Fix issue when using USE\_TZ=False with MySQL [\#353](https://github.com/Koed00/django-q/pull/353) ([maerteijn](https://github.com/maerteijn)) - Document the behaviour of retry value properly [\#340](https://github.com/Koed00/django-q/pull/340) ([janneronkko](https://github.com/janneronkko)) - Fix concurrency issue in timeout timer value processing [\#337](https://github.com/Koed00/django-q/pull/337) ([janneronkko](https://github.com/janneronkko)) - Timeout handling fix and improvements to related tests [\#336](https://github.com/Koed00/django-q/pull/336) ([janneronkko](https://github.com/janneronkko)) - Document how to run tests on your computer [\#334](https://github.com/Koed00/django-q/pull/334) ([janneronkko](https://github.com/janneronkko)) - Updates django version and packages [\#330](https://github.com/Koed00/django-q/pull/330) ([Koed00](https://github.com/Koed00)) - Modified django\_q imports to support Python 3.4 again in cluster.py. â€Ļ [\#327](https://github.com/Koed00/django-q/pull/327) ([mattaw](https://github.com/mattaw)) ## [v1.0.1](https://github.com/koed00/django-q/tree/v1.0.1) (2018-08-29) [Full Changelog](https://github.com/koed00/django-q/compare/v1.0.0...v1.0.1) **Merged pull requests:** - Add locale directory with fr translation [\#312](https://github.com/Koed00/django-q/pull/312) ([tboulogne](https://github.com/tboulogne)) ## [v1.0.0](https://github.com/koed00/django-q/tree/v1.0.0) (2018-08-14) [Full Changelog](https://github.com/koed00/django-q/compare/v0.9.4...v1.0.0) **Closed issues:** - Deleted broken schedules still run [\#308](https://github.com/Koed00/django-q/issues/308) - Python3.7 not supported [\#304](https://github.com/Koed00/django-q/issues/304) - Avoid retrying failed tasks [\#238](https://github.com/Koed00/django-q/issues/238) **Merged pull requests:** - Python 3.7 [\#310](https://github.com/Koed00/django-q/pull/310) ([Koed00](https://github.com/Koed00)) - Fix a typo I introduced in groups.rst [\#309](https://github.com/Koed00/django-q/pull/309) ([P-EB](https://github.com/P-EB)) - Replaces async occurrences with alternatives [\#306](https://github.com/Koed00/django-q/pull/306) ([P-EB](https://github.com/P-EB)) ## [v0.9.4](https://github.com/koed00/django-q/tree/v0.9.4) (2018-03-13) [Full Changelog](https://github.com/koed00/django-q/compare/v0.9.3...v0.9.4) ## [v0.9.3](https://github.com/koed00/django-q/tree/v0.9.3) (2018-03-13) [Full Changelog](https://github.com/koed00/django-q/compare/v0.9.2...v0.9.3) **Closed issues:** - \[Wishlist\] Please provide a changelog in a text format in your repo. [\#293](https://github.com/Koed00/django-q/issues/293) - django-q collides with existing app of name `tasks` [\#199](https://github.com/Koed00/django-q/issues/199) **Merged pull requests:** - Add option for acknowledging failed tasks \(globally and per-task\) [\#298](https://github.com/Koed00/django-q/pull/298) ([Balletie](https://github.com/Balletie)) - Changing the path location where Django-Q is inserted. [\#297](https://github.com/Koed00/django-q/pull/297) ([Eagllus](https://github.com/Eagllus)) ## [v0.9.2](https://github.com/koed00/django-q/tree/v0.9.2) (2018-02-13) [Full Changelog](https://github.com/koed00/django-q/compare/v0.9.1...v0.9.2) **Closed issues:** - \[Debian\] The new release contains a python3-only line. [\#291](https://github.com/Koed00/django-q/issues/291) - Support Python 3.x Only Since 0.9.0? [\#286](https://github.com/Koed00/django-q/issues/286) - Error when using error reporters - AttributeError: 'generator' object has no attribute 'load' [\#276](https://github.com/Koed00/django-q/issues/276) - Question: how to show the running task on the django-admin with redis as broker [\#270](https://github.com/Koed00/django-q/issues/270) - Overflow on repeats fields [\#255](https://github.com/Koed00/django-q/issues/255) - apps.py: Attempted relative import with no known parent package [\#249](https://github.com/Koed00/django-q/issues/249) - Django and Django Q on different server with Redis as broker [\#237](https://github.com/Koed00/django-q/issues/237) - The `django_q/tests` directory isn't in the tarball [\#226](https://github.com/Koed00/django-q/issues/226) - django scrapy project can't connect to redis [\#217](https://github.com/Koed00/django-q/issues/217) - django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. [\#216](https://github.com/Koed00/django-q/issues/216) - Add Sentry support [\#210](https://github.com/Koed00/django-q/issues/210) - Tasks in the queue are not being processed [\#203](https://github.com/Koed00/django-q/issues/203) - Result is - [\#176](https://github.com/Koed00/django-q/issues/176) - ERROR invalid syntax \(\, line 1\) - while passing objects as arguments [\#170](https://github.com/Koed00/django-q/issues/170) - "InterfaceError: connection already closed" being raised when a test is run [\#167](https://github.com/Koed00/django-q/issues/167) - AppRegistryNotReady Exception on Django 1.10 Dev [\#164](https://github.com/Koed00/django-q/issues/164) - Retry possibility? [\#118](https://github.com/Koed00/django-q/issues/118) **Merged pull requests:** - Fix python3 only code [\#292](https://github.com/Koed00/django-q/pull/292) ([Eagllus](https://github.com/Eagllus)) ## [v0.9.1](https://github.com/koed00/django-q/tree/v0.9.1) (2018-02-02) [Full Changelog](https://github.com/koed00/django-q/compare/0.9.0...v0.9.1) **Closed issues:** - Django 2.0 admin last run urls [\#289](https://github.com/Koed00/django-q/issues/289) - The model Schedule is already registered [\#285](https://github.com/Koed00/django-q/issues/285) **Merged pull requests:** - Fix urls being escaped by admin [\#290](https://github.com/Koed00/django-q/pull/290) ([Eagllus](https://github.com/Eagllus)) - fixing entry\_points annotation to expose class rather than module [\#287](https://github.com/Koed00/django-q/pull/287) ([danielwelch](https://github.com/danielwelch)) - Allow SQS to use environment variables [\#283](https://github.com/Koed00/django-q/pull/283) ([svdgraaf](https://github.com/svdgraaf)) ## [0.9.0](https://github.com/koed00/django-q/tree/0.9.0) (2018-01-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.9.0...0.9.0) ## [v0.9.0](https://github.com/koed00/django-q/tree/v0.9.0) (2018-01-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.8.1...v0.9.0) **Closed issues:** - Django-q calls task twice or more [\#183](https://github.com/Koed00/django-q/issues/183) **Merged pull requests:** - Django 2 compatiblity [\#279](https://github.com/Koed00/django-q/pull/279) ([Eagllus](https://github.com/Eagllus)) - fix usage of iter\_entry\_points [\#278](https://github.com/Koed00/django-q/pull/278) ([danielwelch](https://github.com/danielwelch)) - Updates Travis to test for Django 2 and the LTS versions [\#274](https://github.com/Koed00/django-q/pull/274) ([Koed00](https://github.com/Koed00)) - Django 2.0 compatibility [\#269](https://github.com/Koed00/django-q/pull/269) ([achidlow](https://github.com/achidlow)) ## [v0.8.1](https://github.com/koed00/django-q/tree/v0.8.1) (2017-10-12) [Full Changelog](https://github.com/koed00/django-q/compare/v0.8.0...v0.8.1) **Closed issues:** - Django Q tasks are not executed with ./manage.py test [\#266](https://github.com/Koed00/django-q/issues/266) - Unable to delete scheduled task item that was created in the admin. [\#258](https://github.com/Koed00/django-q/issues/258) - \ [\#257](https://github.com/Koed00/django-q/issues/257) - Failed tasks and Chain [\#254](https://github.com/Koed00/django-q/issues/254) - async and chains returns different task id ! [\#244](https://github.com/Koed00/django-q/issues/244) - Python3. Async hook doesn't seem to work [\#240](https://github.com/Koed00/django-q/issues/240) - Workers stall and do nothing [\#239](https://github.com/Koed00/django-q/issues/239) - How is logging handled [\#209](https://github.com/Koed00/django-q/issues/209) - ask close\_old\_connections use! [\#198](https://github.com/Koed00/django-q/issues/198) **Merged pull requests:** - Updates botocore, certifi, chardet, docutils, idna and psutil [\#267](https://github.com/Koed00/django-q/pull/267) ([Koed00](https://github.com/Koed00)) - Replaces some relative imports [\#264](https://github.com/Koed00/django-q/pull/264) ([Koed00](https://github.com/Koed00)) - Updates packages and Django version [\#263](https://github.com/Koed00/django-q/pull/263) ([Koed00](https://github.com/Koed00)) - Use 32 bits integer for repeat field to avoid overflow with frequent scheduled tasks [\#262](https://github.com/Koed00/django-q/pull/262) ([gchardon-hiventy](https://github.com/gchardon-hiventy)) - Error reporter [\#261](https://github.com/Koed00/django-q/pull/261) ([danielwelch](https://github.com/danielwelch)) - Remove dependency `future` [\#247](https://github.com/Koed00/django-q/pull/247) ([benjaoming](https://github.com/benjaoming)) ## [v0.8.0](https://github.com/koed00/django-q/tree/v0.8.0) (2017-04-05) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.18...v0.8.0) **Closed issues:** - How do you actually initiate periodic scheduled tasks in production [\#221](https://github.com/Koed00/django-q/issues/221) - Enhancement: add signals [\#219](https://github.com/Koed00/django-q/issues/219) - very slow performance with global sync: True setting vs. async\(sync=True\) [\#214](https://github.com/Koed00/django-q/issues/214) - daemonic processes are not allowed to have children [\#211](https://github.com/Koed00/django-q/issues/211) - send\_mail problem [\#202](https://github.com/Koed00/django-q/issues/202) - Starting qcluster crashes python in OSX Sierra [\#201](https://github.com/Koed00/django-q/issues/201) - How can I run django-q qcluster with supervisor process manager [\#196](https://github.com/Koed00/django-q/issues/196) - Can't get attribute 'simple\_class\_factory' on module 'django.db.models.base' [\#191](https://github.com/Koed00/django-q/issues/191) - \[Q\] ERROR reincarnated pusher Process-1:4439 after sudden death [\#188](https://github.com/Koed00/django-q/issues/188) - Can not pass async\(\) non-core functions [\#178](https://github.com/Koed00/django-q/issues/178) **Merged pull requests:** - Update to Django 1.11 & Python 3.6 [\#230](https://github.com/Koed00/django-q/pull/230) ([Koed00](https://github.com/Koed00)) - Add django 1.11 support [\#228](https://github.com/Koed00/django-q/pull/228) ([bulv1ne](https://github.com/bulv1ne)) - Update tasks.rst [\#222](https://github.com/Koed00/django-q/pull/222) ([ghost](https://github.com/ghost)) - Add signals [\#220](https://github.com/Koed00/django-q/pull/220) ([abompard](https://github.com/abompard)) - fix a race condition in orm broker [\#213](https://github.com/Koed00/django-q/pull/213) ([yannpom](https://github.com/yannpom)) - Option to undaemonize workers and allows them to spawn child processes [\#212](https://github.com/Koed00/django-q/pull/212) ([yannpom](https://github.com/yannpom)) - Replace global Conf mangling with monkeypatch [\#208](https://github.com/Koed00/django-q/pull/208) ([Urth](https://github.com/Urth)) - Explaining how to handle tasks async with qcluster [\#204](https://github.com/Koed00/django-q/pull/204) ([Eagllus](https://github.com/Eagllus)) - supervisor example in Cluster documentation fix \#196 [\#197](https://github.com/Koed00/django-q/pull/197) ([GabLeRoux](https://github.com/GabLeRoux)) - Update brokers.rst [\#187](https://github.com/Koed00/django-q/pull/187) ([pyprism](https://github.com/pyprism)) - Package update 21 7 [\#184](https://github.com/Koed00/django-q/pull/184) ([Koed00](https://github.com/Koed00)) - Guard loop sleep made configurable [\#182](https://github.com/Koed00/django-q/pull/182) ([bob-r](https://github.com/bob-r)) - Add option to qinfo to print task IDs [\#173](https://github.com/Koed00/django-q/pull/173) ([Aninstance](https://github.com/Aninstance)) - Log id returned by broker [\#148](https://github.com/Koed00/django-q/pull/148) ([k4ml](https://github.com/k4ml)) ## [v0.7.18](https://github.com/koed00/django-q/tree/v0.7.18) (2016-06-07) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.17...v0.7.18) **Closed issues:** - ValueError\('need more than 1 value to unpack',\) [\#171](https://github.com/Koed00/django-q/issues/171) - Successful tasks are not being saved to the database when 'save\_limit' config setting is 0 [\#157](https://github.com/Koed00/django-q/issues/157) **Merged pull requests:** - Updates dependencies [\#175](https://github.com/Koed00/django-q/pull/175) ([Koed00](https://github.com/Koed00)) - Updates Django and some packages [\#169](https://github.com/Koed00/django-q/pull/169) ([Koed00](https://github.com/Koed00)) ## [v0.7.17](https://github.com/koed00/django-q/tree/v0.7.17) (2016-04-24) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.16...v0.7.17) **Closed issues:** - Typo in parameter passed to broker in get\_broker [\#158](https://github.com/Koed00/django-q/issues/158) - Circus stop only stops first process [\#155](https://github.com/Koed00/django-q/issues/155) - Allow task custom name [\#146](https://github.com/Koed00/django-q/issues/146) - Worker recycle causes: "ERROR connection already closed" [\#144](https://github.com/Koed00/django-q/issues/144) **Merged pull requests:** - Updates packages for testing [\#166](https://github.com/Koed00/django-q/pull/166) ([Koed00](https://github.com/Koed00)) - allow scheduler to schedule all pending tasks [\#165](https://github.com/Koed00/django-q/pull/165) ([thatmattbone](https://github.com/thatmattbone)) - Updates docs and tests for new Django versions [\#163](https://github.com/Koed00/django-q/pull/163) ([Koed00](https://github.com/Koed00)) - Fixes typo in custom broker handler [\#159](https://github.com/Koed00/django-q/pull/159) ([Koed00](https://github.com/Koed00)) ## [v0.7.16](https://github.com/koed00/django-q/tree/v0.7.16) (2016-03-07) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.15...v0.7.16) **Fixed bugs:** - "connection already closed" while testing [\#127](https://github.com/Koed00/django-q/issues/127) - Async argument 'timeout' fails if broker timeout is set to None [\#125](https://github.com/Koed00/django-q/issues/125) **Closed issues:** - Scheduled task name behaviour inconsistent [\#150](https://github.com/Koed00/django-q/issues/150) - SystemError: Parent module '' not loaded, cannot perform relative import [\#142](https://github.com/Koed00/django-q/issues/142) - OrmQ broker MySQL connection errors on ORM.delete\(task\_id\) [\#124](https://github.com/Koed00/django-q/issues/124) - Task stays in queue when executing a requests.post [\#97](https://github.com/Koed00/django-q/issues/97) **Merged pull requests:** - Updates Django to 1.9.4 and 1.8.11 [\#153](https://github.com/Koed00/django-q/pull/153) ([Koed00](https://github.com/Koed00)) - Fixes for several issues [\#152](https://github.com/Koed00/django-q/pull/152) ([Koed00](https://github.com/Koed00)) - Updates to Django 1.8.9 and 1.9.2 [\#143](https://github.com/Koed00/django-q/pull/143) ([Koed00](https://github.com/Koed00)) ## [v0.7.15](https://github.com/koed00/django-q/tree/v0.7.15) (2016-01-27) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.14...v0.7.15) **Closed issues:** - Make orm polling interval configurable [\#139](https://github.com/Koed00/django-q/issues/139) **Merged pull requests:** - Adds custom broker setting [\#141](https://github.com/Koed00/django-q/pull/141) ([Koed00](https://github.com/Koed00)) - Adds poll option for database brokers [\#140](https://github.com/Koed00/django-q/pull/140) ([Koed00](https://github.com/Koed00)) ## [v0.7.14](https://github.com/koed00/django-q/tree/v0.7.14) (2016-01-24) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.13...v0.7.14) **Closed issues:** - app file structure [\#100](https://github.com/Koed00/django-q/issues/100) **Merged pull requests:** - Adds task result update [\#138](https://github.com/Koed00/django-q/pull/138) ([Koed00](https://github.com/Koed00)) - Save task now creates or updates [\#136](https://github.com/Koed00/django-q/pull/136) ([Koed00](https://github.com/Koed00)) - Fixes acknowledgement bug for failed tasks [\#135](https://github.com/Koed00/django-q/pull/135) ([Koed00](https://github.com/Koed00)) - Removes duplicate test [\#134](https://github.com/Koed00/django-q/pull/134) ([Koed00](https://github.com/Koed00)) ## [v0.7.13](https://github.com/koed00/django-q/tree/v0.7.13) (2016-01-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.12...v0.7.13) ## [v0.7.12](https://github.com/koed00/django-q/tree/v0.7.12) (2016-01-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.11...v0.7.12) **Closed issues:** - Bug? Hourly schedule\_type runs in reverse [\#128](https://github.com/Koed00/django-q/issues/128) - scheduling repeating tasks [\#121](https://github.com/Koed00/django-q/issues/121) - documentation or design issue? [\#114](https://github.com/Koed00/django-q/issues/114) - Catch\_up States [\#110](https://github.com/Koed00/django-q/issues/110) - foo.bar.tasks.my\_task fails with "No module named bar.tasks" when running in AWS / Elastic Beanstalk [\#105](https://github.com/Koed00/django-q/issues/105) - Q Cluster auto-launch? [\#102](https://github.com/Koed00/django-q/issues/102) **Merged pull requests:** - v0.7.12 [\#132](https://github.com/Koed00/django-q/pull/132) ([Koed00](https://github.com/Koed00)) - Adds Rollbar support for exceptions [\#131](https://github.com/Koed00/django-q/pull/131) ([Koed00](https://github.com/Koed00)) - Updates packages and Django for testing [\#130](https://github.com/Koed00/django-q/pull/130) ([Koed00](https://github.com/Koed00)) - Fix for unusable ORM Broker connections [\#126](https://github.com/Koed00/django-q/pull/126) ([kdmukai](https://github.com/kdmukai)) - Adds a check for duplicate schedule names. [\#123](https://github.com/Koed00/django-q/pull/123) ([Koed00](https://github.com/Koed00)) - fixed typo [\#117](https://github.com/Koed00/django-q/pull/117) ([Eagllus](https://github.com/Eagllus)) - Update example to use string for the schedule\_type [\#115](https://github.com/Koed00/django-q/pull/115) ([Eagllus](https://github.com/Eagllus)) - Updates Blessed [\#113](https://github.com/Koed00/django-q/pull/113) ([Koed00](https://github.com/Koed00)) - docs: rephrased the missing schedule description [\#112](https://github.com/Koed00/django-q/pull/112) ([Koed00](https://github.com/Koed00)) - Updates botocore for testing [\#111](https://github.com/Koed00/django-q/pull/111) ([Koed00](https://github.com/Koed00)) - Updates Django [\#109](https://github.com/Koed00/django-q/pull/109) ([Koed00](https://github.com/Koed00)) - Updates botocore for testing [\#108](https://github.com/Koed00/django-q/pull/108) ([Koed00](https://github.com/Koed00)) ## [v0.7.11](https://github.com/koed00/django-q/tree/v0.7.11) (2015-10-28) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.10...v0.7.11) **Closed issues:** - fetch\(\) only works for the first task, returning None for subsequent tasks IDs [\#99](https://github.com/Koed00/django-q/issues/99) **Merged pull requests:** - docs: added Async class [\#104](https://github.com/Koed00/django-q/pull/104) ([Koed00](https://github.com/Koed00)) - adds `Async` class [\#103](https://github.com/Koed00/django-q/pull/103) ([Koed00](https://github.com/Koed00)) - Adds timeout to group key cache object [\#98](https://github.com/Koed00/django-q/pull/98) ([Koed00](https://github.com/Koed00)) ## [v0.7.10](https://github.com/koed00/django-q/tree/v0.7.10) (2015-10-19) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.9...v0.7.10) **Merged pull requests:** - Adds task chains [\#96](https://github.com/Koed00/django-q/pull/96) ([Koed00](https://github.com/Koed00)) - Updates botocore for testing [\#95](https://github.com/Koed00/django-q/pull/95) ([Koed00](https://github.com/Koed00)) - Updates Requests and Blessed requirements for testing [\#94](https://github.com/Koed00/django-q/pull/94) ([Koed00](https://github.com/Koed00)) - Updates Arrow and Blessed for testing [\#93](https://github.com/Koed00/django-q/pull/93) ([Koed00](https://github.com/Koed00)) - Updates botocore for testing [\#92](https://github.com/Koed00/django-q/pull/92) ([Koed00](https://github.com/Koed00)) ## [v0.7.9](https://github.com/koed00/django-q/tree/v0.7.9) (2015-10-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.8...v0.7.9) **Merged pull requests:** - Updated botocore for testing [\#91](https://github.com/Koed00/django-q/pull/91) ([Koed00](https://github.com/Koed00)) - adds version info to qinfo [\#90](https://github.com/Koed00/django-q/pull/90) ([Koed00](https://github.com/Koed00)) - Adds version and broker info to qinfo [\#89](https://github.com/Koed00/django-q/pull/89) ([Koed00](https://github.com/Koed00)) - Updates botocore, requests and six for testing [\#88](https://github.com/Koed00/django-q/pull/88) ([Koed00](https://github.com/Koed00)) - adds `cached` option to `async_iter` [\#87](https://github.com/Koed00/django-q/pull/87) ([Koed00](https://github.com/Koed00)) - moves hook signal in separate module [\#86](https://github.com/Koed00/django-q/pull/86) ([Koed00](https://github.com/Koed00)) - Updates psutil to 3.2.2 [\#85](https://github.com/Koed00/django-q/pull/85) ([Koed00](https://github.com/Koed00)) ## [v0.7.8](https://github.com/koed00/django-q/tree/v0.7.8) (2015-10-04) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.7...v0.7.8) **Merged pull requests:** - Adds cached result backend [\#84](https://github.com/Koed00/django-q/pull/84) ([Koed00](https://github.com/Koed00)) - Update tests for Django 1.8.5 [\#83](https://github.com/Koed00/django-q/pull/83) ([Koed00](https://github.com/Koed00)) - updates botocore to 1.2.6 for testing [\#81](https://github.com/Koed00/django-q/pull/81) ([Koed00](https://github.com/Koed00)) ## [v0.7.7](https://github.com/koed00/django-q/tree/v0.7.7) (2015-09-29) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.6...v0.7.7) ## [v0.7.6](https://github.com/koed00/django-q/tree/v0.7.6) (2015-09-29) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.5...v0.7.6) **Closed issues:** - Cluster dies when started with Postgres [\#79](https://github.com/Koed00/django-q/issues/79) **Merged pull requests:** - \#79 close django db connection before fork [\#80](https://github.com/Koed00/django-q/pull/80) ([Koed00](https://github.com/Koed00)) ## [v0.7.5](https://github.com/koed00/django-q/tree/v0.7.5) (2015-09-28) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.4...v0.7.5) **Closed issues:** - Getting "MySQL server has gone away" on new tasks after idling [\#76](https://github.com/Koed00/django-q/issues/76) **Merged pull requests:** - docs: mention Django 1.9a1 support [\#78](https://github.com/Koed00/django-q/pull/78) ([Koed00](https://github.com/Koed00)) - Adds stale db connection check before every transaction [\#77](https://github.com/Koed00/django-q/pull/77) ([Koed00](https://github.com/Koed00)) ## [v0.7.4](https://github.com/koed00/django-q/tree/v0.7.4) (2015-09-26) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.3...v0.7.4) **Merged pull requests:** - Adds MongoDB broker [\#75](https://github.com/Koed00/django-q/pull/75) ([Koed00](https://github.com/Koed00)) - Removes root imports [\#74](https://github.com/Koed00/django-q/pull/74) ([Koed00](https://github.com/Koed00)) - Removes pycharm stdin bug workaround [\#73](https://github.com/Koed00/django-q/pull/73) ([Koed00](https://github.com/Koed00)) - adds compatibility section to docs [\#72](https://github.com/Koed00/django-q/pull/72) ([Koed00](https://github.com/Koed00)) - Only show lock count when available and greater than zero [\#71](https://github.com/Koed00/django-q/pull/71) ([Koed00](https://github.com/Koed00)) - Python 3.5 compatibility [\#70](https://github.com/Koed00/django-q/pull/70) ([Koed00](https://github.com/Koed00)) - docs: Replaced redis pooling with broker pooling example. [\#69](https://github.com/Koed00/django-q/pull/69) ([Koed00](https://github.com/Koed00)) ## [v0.7.3](https://github.com/koed00/django-q/tree/v0.7.3) (2015-09-18) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.2...v0.7.3) **Merged pull requests:** - Adds a wait option for results. [\#68](https://github.com/Koed00/django-q/pull/68) ([Koed00](https://github.com/Koed00)) ## [v0.7.2](https://github.com/koed00/django-q/tree/v0.7.2) (2015-09-17) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.1...v0.7.2) **Merged pull requests:** - orm: Improves locking behavior [\#67](https://github.com/Koed00/django-q/pull/67) ([Koed00](https://github.com/Koed00)) - tests orm queue admin overrides [\#66](https://github.com/Koed00/django-q/pull/66) ([Koed00](https://github.com/Koed00)) - Adds remote orm admin view [\#65](https://github.com/Koed00/django-q/pull/65) ([Koed00](https://github.com/Koed00)) ## [v0.7.1](https://github.com/koed00/django-q/tree/v0.7.1) (2015-09-16) [Full Changelog](https://github.com/koed00/django-q/compare/v0.7.0...v0.7.1) **Merged pull requests:** - Adds configuration output and other enhancements [\#64](https://github.com/Koed00/django-q/pull/64) ([Koed00](https://github.com/Koed00)) ## [v0.7.0](https://github.com/koed00/django-q/tree/v0.7.0) (2015-09-14) [Full Changelog](https://github.com/koed00/django-q/compare/v0.6.4...v0.7.0) **Merged pull requests:** - Adds Django ORM broker [\#63](https://github.com/Koed00/django-q/pull/63) ([Koed00](https://github.com/Koed00)) - Updated wcwidth [\#62](https://github.com/Koed00/django-q/pull/62) ([Koed00](https://github.com/Koed00)) - Adds Fastack option to Disque broker [\#61](https://github.com/Koed00/django-q/pull/61) ([Koed00](https://github.com/Koed00)) - Updates test dependencies [\#60](https://github.com/Koed00/django-q/pull/60) ([Koed00](https://github.com/Koed00)) ## [v0.6.4](https://github.com/koed00/django-q/tree/v0.6.4) (2015-09-10) [Full Changelog](https://github.com/koed00/django-q/compare/v0.6.3...v0.6.4) **Closed issues:** - `qcluster` command doesn't handle interrupts. [\#56](https://github.com/Koed00/django-q/issues/56) **Merged pull requests:** - Adds Amazon SQS broker [\#58](https://github.com/Koed00/django-q/pull/58) ([Koed00](https://github.com/Koed00)) - \#56 cpu\_affinity not supported on some platforms [\#57](https://github.com/Koed00/django-q/pull/57) ([Koed00](https://github.com/Koed00)) - docs: `cache` option [\#55](https://github.com/Koed00/django-q/pull/55) ([Koed00](https://github.com/Koed00)) ## [v0.6.3](https://github.com/koed00/django-q/tree/v0.6.3) (2015-09-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.6.2...v0.6.3) **Merged pull requests:** - Adds IronMQ broker [\#54](https://github.com/Koed00/django-q/pull/54) ([Koed00](https://github.com/Koed00)) ## [v0.6.2](https://github.com/koed00/django-q/tree/v0.6.2) (2015-09-07) [Full Changelog](https://github.com/koed00/django-q/compare/v0.6.1...v0.6.2) **Merged pull requests:** - Fixes backward compatibility problems with django-picklefield [\#53](https://github.com/Koed00/django-q/pull/53) ([Koed00](https://github.com/Koed00)) ## [v0.6.1](https://github.com/koed00/django-q/tree/v0.6.1) (2015-09-07) [Full Changelog](https://github.com/koed00/django-q/compare/v0.6.0...v0.6.1) ## [v0.6.0](https://github.com/koed00/django-q/tree/v0.6.0) (2015-09-06) [Full Changelog](https://github.com/koed00/django-q/compare/v0.5.3...v0.6.0) **Merged pull requests:** - Adds pluggable brokers [\#52](https://github.com/Koed00/django-q/pull/52) ([Koed00](https://github.com/Koed00)) - \#50 adds psutil as alternative os.getppid provider [\#51](https://github.com/Koed00/django-q/pull/51) ([Koed00](https://github.com/Koed00)) ## [v0.5.3](https://github.com/koed00/django-q/tree/v0.5.3) (2015-08-19) [Full Changelog](https://github.com/koed00/django-q/compare/v0.5.2...v0.5.3) **Merged pull requests:** - v0.5.3 [\#49](https://github.com/Koed00/django-q/pull/49) ([Koed00](https://github.com/Koed00)) - adds `catch_up` configuration option [\#48](https://github.com/Koed00/django-q/pull/48) ([Koed00](https://github.com/Koed00)) - consolidates redis ping [\#47](https://github.com/Koed00/django-q/pull/47) ([Koed00](https://github.com/Koed00)) ## [v0.5.2](https://github.com/koed00/django-q/tree/v0.5.2) (2015-08-13) [Full Changelog](https://github.com/koed00/django-q/compare/v0.5.1...v0.5.2) **Merged pull requests:** - Adds global `sync` configuration option [\#46](https://github.com/Koed00/django-q/pull/46) ([Koed00](https://github.com/Koed00)) ## [v0.5.1](https://github.com/koed00/django-q/tree/v0.5.1) (2015-08-12) [Full Changelog](https://github.com/koed00/django-q/compare/v0.5.0...v0.5.1) **Merged pull requests:** - Adds `qinfo` management command [\#45](https://github.com/Koed00/django-q/pull/45) ([Koed00](https://github.com/Koed00)) ## [v0.5.0](https://github.com/koed00/django-q/tree/v0.5.0) (2015-08-06) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.6...v0.5.0) **Closed issues:** - Too many workers [\#43](https://github.com/Koed00/django-q/issues/43) **Merged pull requests:** - Adds a n-minutes option to the scheduler [\#44](https://github.com/Koed00/django-q/pull/44) ([Koed00](https://github.com/Koed00)) ## [v0.4.6](https://github.com/koed00/django-q/tree/v0.4.6) (2015-08-04) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.5...v0.4.6) **Closed issues:** - sem\_getvalue not implemented on OSX [\#40](https://github.com/Koed00/django-q/issues/40) **Merged pull requests:** - Replaces qsize == 0 with empty\(\) [\#42](https://github.com/Koed00/django-q/pull/42) ([Koed00](https://github.com/Koed00)) - Workaround for osx implementation [\#41](https://github.com/Koed00/django-q/pull/41) ([Koed00](https://github.com/Koed00)) ## [v0.4.5](https://github.com/koed00/django-q/tree/v0.4.5) (2015-08-01) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.4...v0.4.5) **Closed issues:** - Getting 'can't pickle lock objects' using async\(\) [\#38](https://github.com/Koed00/django-q/issues/38) **Merged pull requests:** - Sets pickle protocol to highest [\#39](https://github.com/Koed00/django-q/pull/39) ([Koed00](https://github.com/Koed00)) - fixes save\_limit +1 [\#37](https://github.com/Koed00/django-q/pull/37) ([Koed00](https://github.com/Koed00)) - Moves unpacking task from Worker to Pusher [\#36](https://github.com/Koed00/django-q/pull/36) ([Koed00](https://github.com/Koed00)) ## [v0.4.4](https://github.com/koed00/django-q/tree/v0.4.4) (2015-07-27) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.3...v0.4.4) **Merged pull requests:** - closes old db connections on monitor and worker spawn [\#35](https://github.com/Koed00/django-q/pull/35) ([Koed00](https://github.com/Koed00)) - Updated to future 0.15.0 [\#34](https://github.com/Koed00/django-q/pull/34) ([Koed00](https://github.com/Koed00)) - Group filter and queue limit indicator [\#33](https://github.com/Koed00/django-q/pull/33) ([Koed00](https://github.com/Koed00)) ## [v0.4.3](https://github.com/koed00/django-q/tree/v0.4.3) (2015-07-24) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.2...v0.4.3) **Merged pull requests:** - Adds queue limit and encryption salt [\#32](https://github.com/Koed00/django-q/pull/32) ([Koed00](https://github.com/Koed00)) - adds Haystack example [\#31](https://github.com/Koed00/django-q/pull/31) ([Koed00](https://github.com/Koed00)) ## [v0.4.2](https://github.com/koed00/django-q/tree/v0.4.2) (2015-07-22) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.1...v0.4.2) **Closed issues:** - Timeout doesn't work [\#28](https://github.com/Koed00/django-q/issues/28) **Merged pull requests:** - Minor linting and fixes [\#30](https://github.com/Koed00/django-q/pull/30) ([Koed00](https://github.com/Koed00)) - timeout as float [\#29](https://github.com/Koed00/django-q/pull/29) ([Koed00](https://github.com/Koed00)) ## [v0.4.1](https://github.com/koed00/django-q/tree/v0.4.1) (2015-07-21) [Full Changelog](https://github.com/koed00/django-q/compare/v0.4.0...v0.4.1) **Merged pull requests:** - Adds `save` override options for tasks [\#27](https://github.com/Koed00/django-q/pull/27) ([Koed00](https://github.com/Koed00)) - Expanding coverage [\#26](https://github.com/Koed00/django-q/pull/26) ([Koed00](https://github.com/Koed00)) ## [v0.4.0](https://github.com/koed00/django-q/tree/v0.4.0) (2015-07-19) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.5...v0.4.0) **Merged pull requests:** - Added a group example [\#25](https://github.com/Koed00/django-q/pull/25) ([Koed00](https://github.com/Koed00)) - Adds failure filtering to group functions [\#24](https://github.com/Koed00/django-q/pull/24) ([Koed00](https://github.com/Koed00)) - Adds count\_group\(\) and delete\_group\(\) [\#23](https://github.com/Koed00/django-q/pull/23) ([Koed00](https://github.com/Koed00)) - decoding values\_list on a picklefield is faster [\#22](https://github.com/Koed00/django-q/pull/22) ([Koed00](https://github.com/Koed00)) - Adds task groups [\#21](https://github.com/Koed00/django-q/pull/21) ([Koed00](https://github.com/Koed00)) ## [v0.3.5](https://github.com/koed00/django-q/tree/v0.3.5) (2015-07-17) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.6...v0.3.5) ## [v0.3.6](https://github.com/koed00/django-q/tree/v0.3.6) (2015-07-17) [Full Changelog](https://github.com/koed00/django-q/compare/0.3.5...v0.3.6) **Merged pull requests:** - Tests now run with logging level debug [\#20](https://github.com/Koed00/django-q/pull/20) ([Koed00](https://github.com/Koed00)) - docs: small edits [\#19](https://github.com/Koed00/django-q/pull/19) ([Koed00](https://github.com/Koed00)) - Adds a `timeout` override per task [\#18](https://github.com/Koed00/django-q/pull/18) ([Koed00](https://github.com/Koed00)) - Adds management commands to the tests [\#17](https://github.com/Koed00/django-q/pull/17) ([Koed00](https://github.com/Koed00)) - Docs: Added a report example [\#16](https://github.com/Koed00/django-q/pull/16) ([Koed00](https://github.com/Koed00)) ## [0.3.5](https://github.com/koed00/django-q/tree/0.3.5) (2015-07-15) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.4...0.3.5) **Merged pull requests:** - Adds cpu affinity to workers [\#15](https://github.com/Koed00/django-q/pull/15) ([Koed00](https://github.com/Koed00)) - Clean up redis key after test [\#14](https://github.com/Koed00/django-q/pull/14) ([Koed00](https://github.com/Koed00)) - Adding sphinx build to Travis [\#13](https://github.com/Koed00/django-q/pull/13) ([Koed00](https://github.com/Koed00)) - Adding examples to the docs [\#12](https://github.com/Koed00/django-q/pull/12) ([Koed00](https://github.com/Koed00)) ## [v0.3.4](https://github.com/koed00/django-q/tree/v0.3.4) (2015-07-12) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.3...v0.3.4) **Merged pull requests:** - Testing with Arrow 0.6.0 now [\#11](https://github.com/Koed00/django-q/pull/11) ([Koed00](https://github.com/Koed00)) - Schedules of type ONCE will selfdestruct with negative repeats [\#10](https://github.com/Koed00/django-q/pull/10) ([Koed00](https://github.com/Koed00)) ## [v0.3.3](https://github.com/koed00/django-q/tree/v0.3.3) (2015-07-10) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.2...v0.3.3) **Closed issues:** - Documentation for mocking in case of testing [\#7](https://github.com/Koed00/django-q/issues/7) **Merged pull requests:** - Fixes save pruning bug [\#9](https://github.com/Koed00/django-q/pull/9) ([Koed00](https://github.com/Koed00)) ## [v0.3.2](https://github.com/koed00/django-q/tree/v0.3.2) (2015-07-09) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.1...v0.3.2) **Closed issues:** - No module named builtins [\#4](https://github.com/Koed00/django-q/issues/4) **Merged pull requests:** - Updated docs [\#6](https://github.com/Koed00/django-q/pull/6) ([Koed00](https://github.com/Koed00)) - Added 'future' to setup.py dependencies [\#5](https://github.com/Koed00/django-q/pull/5) ([nickpolet](https://github.com/nickpolet)) ## [v0.3.1](https://github.com/koed00/django-q/tree/v0.3.1) (2015-07-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.3.0...v0.3.1) ## [v0.3.0](https://github.com/koed00/django-q/tree/v0.3.0) (2015-07-08) [Full Changelog](https://github.com/koed00/django-q/compare/v0.2.2...v0.3.0) **Merged pull requests:** - Switched to uuid4 instead of luid [\#3](https://github.com/Koed00/django-q/pull/3) ([Koed00](https://github.com/Koed00)) ## [v0.2.2](https://github.com/koed00/django-q/tree/v0.2.2) (2015-07-07) [Full Changelog](https://github.com/koed00/django-q/compare/v0.2.1.1...v0.2.2) **Merged pull requests:** - Stabilizing stop procedures [\#2](https://github.com/Koed00/django-q/pull/2) ([Koed00](https://github.com/Koed00)) ## [v0.2.1.1](https://github.com/koed00/django-q/tree/v0.2.1.1) (2015-07-06) [Full Changelog](https://github.com/koed00/django-q/compare/v0.2.1...v0.2.1.1) ## [v0.2.1](https://github.com/koed00/django-q/tree/v0.2.1) (2015-07-06) [Full Changelog](https://github.com/koed00/django-q/compare/v0.2.0...v0.2.1) ## [v0.2.0](https://github.com/koed00/django-q/tree/v0.2.0) (2015-07-04) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.4.1...v0.2.0) ## [v0.1.4.1](https://github.com/koed00/django-q/tree/v0.1.4.1) (2015-07-02) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.4...v0.1.4.1) ## [v0.1.4](https://github.com/koed00/django-q/tree/v0.1.4) (2015-07-01) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.3...v0.1.4) ## [v0.1.3](https://github.com/koed00/django-q/tree/v0.1.3) (2015-06-30) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.2...v0.1.3) ## [v0.1.2](https://github.com/koed00/django-q/tree/v0.1.2) (2015-06-30) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.1...v0.1.2) ## [v0.1.1](https://github.com/koed00/django-q/tree/v0.1.1) (2015-06-28) [Full Changelog](https://github.com/koed00/django-q/compare/v0.1.0...v0.1.1) ## [v0.1.0](https://github.com/koed00/django-q/tree/v0.1.0) (2015-06-28) [Full Changelog](https://github.com/koed00/django-q/compare/c74f931930f9124205c1a4d9ba51700909d43b88...v0.1.0) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* django-q2-1.7.4/Dockerfile000066400000000000000000000002041471170400300152700ustar00rootroot00000000000000FROM python:3.12 ENV PYTHONUNBUFFERED 1 RUN mkdir -p /app WORKDIR /app COPY . . RUN pip install django blessed django-picklefield django-q2-1.7.4/Dockerfile.dev000066400000000000000000000007731471170400300160600ustar00rootroot00000000000000# Sets the python version FROM python:3.9.5-slim # Allows the logs generated by python apps to be rendered in the terminal ENV PYTHONUNBUFFERED 1 # Sets the default shell to bash ENV SHELL /bin/bash RUN set -ex \ && apt update \ && apt-get install gcc python3-dev --yes # Upgrades pip RUN pip install -U pip setuptools # Install poetry RUN pip install poetry==1.8.2 WORKDIR /app COPY . . RUN poetry lock --no-update RUN poetry config virtualenvs.create false RUN poetry install -E testing django-q2-1.7.4/LICENSE000066400000000000000000000021001471170400300143000ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 - 2021 Ilan Steemers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-q2-1.7.4/Makefile000066400000000000000000000014731471170400300147470ustar00rootroot00000000000000dev: docker compose -f web-docker-compose.yaml up test: docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest shell: docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py shell makemigrations: docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py makemigrations migrate: docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py migrate createsuperuser: docker compose -f web-docker-compose.yaml run --rm web python manage.py createsuperuser format: docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run ruff format . docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run ruff check . --fix django-q2-1.7.4/README.rst000066400000000000000000000157331471170400300150020ustar00rootroot00000000000000A multiprocessing distributed task queue for Django --------------------------------------------------- |image0| |image1| |downloads| Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q Features ~~~~~~~~ - Multiprocessing worker pool - Asynchronous tasks - Scheduled, cron and repeated tasks - Signed and compressed packages - Failure and success database or cache - Result hooks, groups and chains - Django Admin integration - PaaS compatible with multiple instances - Multi cluster monitor - Redis, IronMQ, SQS, MongoDB or ORM - Rollbar and Sentry support Changes compared to the original Django-Q: - Dropped support for Disque (hasn't been updated in a long time) - Dropped Redis, Arrow and Blessed dependencies - Updated all current dependencies - Added tests for Django 4.x and 5.x - Added Turkish language - Improved admin area - Fixed a lot of issues See the `changelog `__ for all changes. Requirements ~~~~~~~~~~~~ - `Django `__ > = 4.2 - `Django-picklefield `__ Tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.X and 5.0.X Brokers ~~~~~~~ - `Redis `__ - `IronMQ `__ - `Amazon SQS `__ - `MongoDB `__ - `Django ORM `__ Installation ~~~~~~~~~~~~ - Install the latest version with pip:: $ pip install django-q2 - Add `django_q` to your `INSTALLED_APPS` in your projects `settings.py`:: INSTALLED_APPS = ( # other apps 'django_q', ) - Run Django migrations to create the database tables:: $ python manage.py migrate - Choose a message `broker `__, configure and install the appropriate client library. Read the full documentation at `https://django-q2.readthedocs.org `__ Configuration ~~~~~~~~~~~~~ All configuration settings are optional. e.g: .. code:: python # settings.py example Q_CLUSTER = { 'name': 'myproject', 'workers': 8, 'recycle': 500, 'timeout': 60, 'compress': True, 'cpu_affinity': 1, 'save_limit': 250, 'queue_limit': 500, 'label': 'Django Q', 'redis': { 'host': '127.0.0.1', 'port': 6379, 'db': 0, } } For full configuration options, see the `configuration documentation `__. Management Commands ~~~~~~~~~~~~~~~~~~~ For the management commands to work, you will need to install Blessed: Start a cluster with:: $ python manage.py qcluster Monitor your clusters with:: $ python manage.py qmonitor Monitor your clusters' memory usage with:: $ python manage.py qmemory Check overall statistics with:: $ python manage.py qinfo Creating Tasks ~~~~~~~~~~~~~~ Use `async_task` from your code to quickly offload tasks: .. code:: python from django_q.tasks import async_task, result # create the task async_task('math.copysign', 2, -2) # or with a reference import math.copysign task_id = async_task(copysign, 2, -2) # get the result task_result = result(task_id) # result returns None if the task has not been executed yet # you can wait for it task_result = result(task_id, 200) # but in most cases you will want to use a hook: async_task('math.modf', 2.5, hook='hooks.print_result') # hooks.py def print_result(task): print(task.result) For more info see `Tasks `__ Schedule ~~~~~~~~ Schedules are regular Django models. You can manage them through the Admin page or directly from your code: .. code:: python # Use the schedule function from django_q.tasks import schedule schedule('math.copysign', 2, -2, hook='hooks.print_result', schedule_type=Schedule.DAILY) # Or create the object directly from django_q.models import Schedule Schedule.objects.create(func='math.copysign', hook='hooks.print_result', args='2,-2', schedule_type=Schedule.DAILY ) # Run a task every 5 minutes, starting at 6 today # for 2 hours from datetime import datetime schedule('math.hypot', 3, 4, schedule_type=Schedule.MINUTES, minutes=5, repeats=24, next_run=datetime.utcnow().replace(hour=18, minute=0)) # Use a cron expression schedule('math.hypot', 3, 4, schedule_type=Schedule.CRON, cron = '0 22 * * 1-5') For more info check the `Schedules `__ documentation. Development ~~~~~~~~~~~ There is an example project that you can use to develop with. Docker (compose) is being used to set everything up. Please note that you will have to restart the django-q container when changes have been made to tasks or django-q. You can start the example project with: .. code:: bash make dev Create a superuser with: .. code:: bash make createsuperuser Testing ~~~~~~~ Running tests is easy with docker compose, it will also start the necessary databases. Just run: .. code:: bash make test Locale ~~~~~~ Currently available in English, German, Turkish, and French. Translation pull requests are always welcome. Acknowledgements ~~~~~~~~~~~~~~~~ - Django Q was inspired by working with `Django-RQ `__ and `RQ `__ - Human readable hashes by `HumanHash `__ - Redditors feedback at `r/django `__ - JetBrains for their `Open Source Support Program `__ .. |image0| image:: https://github.com/GDay/django-q2/actions/workflows/test.yml/badge.svg?branche=master :target: https://github.com/GDay/django-q2/actions?query=workflow%3Atests .. |image1| image:: https://coveralls.io/repos/github/GDay/django-q2/badge.svg?branch=master :target: https://coveralls.io/github/GDay/django-q2?branch=master .. |downloads| image:: https://img.shields.io/pypi/dm/django-q2 :target: https://img.shields.io/pypi/dm/django-q2 django-q2-1.7.4/containers/000077500000000000000000000000001471170400300154475ustar00rootroot00000000000000django-q2-1.7.4/containers/localstack/000077500000000000000000000000001471170400300175675ustar00rootroot00000000000000django-q2-1.7.4/containers/localstack/init.sh000077500000000000000000000014211471170400300210670ustar00rootroot00000000000000#!/bin/bash # Note that this file needs to have the executable bit set for it to work with later localstack implementations. export DEFAULT_REGION=us-west-2 create_sqs() { QUEUE_NAME="$1" TIMEOUT=${2:-60} DL_QUEUE_URL=$(awslocal sqs create-queue --queue-name "dl-$QUEUE_NAME" --query QueueUrl --output text) echo ">>> Created $DL_QUEUE_URL queue!" DL_QUEUE_ARN=$(awslocal sqs get-queue-attributes --queue-url "$DL_QUEUE_URL" --attribute-names QueueArn --query Attributes.QueueArn --output text) awslocal sqs create-queue --queue-name "$QUEUE_NAME" --attributes '{ "RedrivePolicy": "{\"deadLetterTargetArn\": \"'"$DL_QUEUE_ARN"'\",\"maxReceiveCount\":\"3\"}", "VisibilityTimeout": "'"$TIMEOUT"'" }' } # Create SQS queues create_sqs testingdjango-q2-1.7.4/django_compilemessages.py000066400000000000000000000002321471170400300203530ustar00rootroot00000000000000import subprocess def generate_mo_files(): subprocess.run(["django-admin", "compilemessages"]) if __name__ == "__main__": generate_mo_files() django-q2-1.7.4/django_q/000077500000000000000000000000001471170400300150645ustar00rootroot00000000000000django-q2-1.7.4/django_q/__init__.py000066400000000000000000000002511471170400300171730ustar00rootroot00000000000000import django VERSION = (1, 7, 4) if django.VERSION < (3, 2): default_app_config = "django_q.apps.DjangoQConfig" __all__ = ["conf", "cluster", "models", "tasks"] django-q2-1.7.4/django_q/admin.py000066400000000000000000000114471471170400300165350ustar00rootroot00000000000000"""Admin module for Django.""" from django.contrib import admin from django.db.models.expressions import OuterRef, Subquery from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django_q.conf import Conf, croniter from django_q.models import Failure, OrmQ, Schedule, Success, Task from django_q.tasks import async_task def resubmit_task(model_admin, request, queryset): """Submit selected tasks back to the queue.""" for task in queryset: async_task( task.func, *task.args or (), hook=task.hook, group=task.group, cluster=task.cluster, **task.kwargs or {}, ) if isinstance(model_admin, FailAdmin): task.delete() resubmit_task.short_description = _("Resubmit selected tasks to queue") class TaskAdmin(admin.ModelAdmin): """model admin for success tasks.""" list_display = ( "name", "group", "func", "cluster", "started", "stopped", "time_taken", ) actions = [resubmit_task] def has_add_permission(self, request): """Don't allow adds.""" return False def get_queryset(self, request): """Only show successes.""" qs = super(TaskAdmin, self).get_queryset(request) return qs.filter(success=True) search_fields = ("name", "func", "group") readonly_fields = [] list_filter = ("group", "cluster") def get_readonly_fields(self, request, obj=None): """Set all fields readonly.""" return list(self.readonly_fields) + [field.name for field in obj._meta.fields] class FailAdmin(admin.ModelAdmin): """model admin for failed tasks.""" list_display = ( "name", "group", "func", "cluster", "started", "stopped", "short_result", ) def has_add_permission(self, request): """Don't allow adds.""" return False actions = [resubmit_task] search_fields = ("name", "func", "group") list_filter = ("group", "cluster") readonly_fields = [] def get_readonly_fields(self, request, obj=None): """Set all fields readonly.""" return list(self.readonly_fields) + [field.name for field in obj._meta.fields] class ScheduleAdmin(admin.ModelAdmin): """model admin for schedules""" list_display = ( "id", "name", "func", "schedule_type", "repeats", "cluster", "next_run", "get_last_run", "get_success", ) # optional cron strings if not croniter: readonly_fields = ("cron",) list_filter = ("next_run", "schedule_type", "cluster") search_fields = ( "name", "func", ) list_display_links = ("id", "name") def get_queryset(self, request): qs = super().get_queryset(request) task_query = Task.objects.filter(id=OuterRef("task")).values( "id", "name", "success" ) qs = qs.annotate( task_id=Subquery(task_query.values("id")), task_name=Subquery(task_query.values("name")), task_success=Subquery(task_query.values("success")), ) return qs def get_success(self, obj): return obj.task_success get_success.boolean = True get_success.short_description = _("success") def get_last_run(self, obj): if obj.task_name is not None: if obj.task_success: url = reverse("admin:django_q_success_change", args=(obj.task_id,)) else: url = reverse("admin:django_q_failure_change", args=(obj.task_id,)) return format_html(f'[{obj.task_name}]') return None get_last_run.allow_tags = True get_last_run.short_description = _("last_run") class QueueAdmin(admin.ModelAdmin): """queue admin for ORM broker""" list_display = ("id", "key", "name", "group", "func", "lock", "task_id") fields = ( "key", "lock", "task_id", "name", "group", "func", "args", "kwargs", "q_options", ) readonly_fields = fields[2:] def save_model(self, request, obj, form, change): obj.save(using=Conf.ORM) def delete_model(self, request, obj): obj.delete(using=Conf.ORM) def get_queryset(self, request): return super(QueueAdmin, self).get_queryset(request).using(Conf.ORM) def has_add_permission(self, request): """Don't allow adds.""" return False list_filter = ("key",) admin.site.register(Schedule, ScheduleAdmin) admin.site.register(Success, TaskAdmin) admin.site.register(Failure, FailAdmin) if Conf.ORM or Conf.TESTING: admin.site.register(OrmQ, QueueAdmin) django-q2-1.7.4/django_q/apps.py000066400000000000000000000004411471170400300164000ustar00rootroot00000000000000from django.apps import AppConfig from django_q.conf import Conf class DjangoQConfig(AppConfig): name = "django_q" verbose_name = Conf.LABEL default_auto_field = "django.db.models.AutoField" def ready(self): from django_q.signals import call_hook # noqa: F401 django-q2-1.7.4/django_q/brokers/000077500000000000000000000000001471170400300165335ustar00rootroot00000000000000django-q2-1.7.4/django_q/brokers/__init__.py000066400000000000000000000120551471170400300206470ustar00rootroot00000000000000import importlib from typing import Optional from django.core.cache import InvalidCacheBackendError, caches from django_q.conf import Conf class Broker: def __init__(self, list_key: str = None): # With same BROKER_CLASS, `list_key` is just a synonym for `queue_name` except for RedisBroker list_key = list_key or Conf.CLUSTER_NAME self.connection = self.get_connection(list_key) self.list_key = list_key self.cache = self.get_cache() self._info = None def __getstate__(self): return self.list_key, self._info def __setstate__(self, state): self.list_key, self._info = state self.connection = self.get_connection(self.list_key) self.cache = self.get_cache() def enqueue(self, task): """ Puts a task onto the queue :type task: str :return: task id """ pass def dequeue(self): """ Gets a task from the queue :return: tuple with task id and task message """ pass def queue_size(self): """ :return: the amount of tasks in the queue """ pass def lock_size(self): """ :return: the number of tasks currently awaiting acknowledgement """ def delete_queue(self): """ Deletes the queue from the broker """ pass def purge_queue(self): """ Purges the queue of any tasks """ pass def delete(self, task_id): """ Deletes a task from the queue :param task_id: the id of the task """ pass def acknowledge(self, task_id): """ Acknowledges completion of the task and removes it from the queue. :param task_id: the id of the task """ pass def fail(self, task_id): """ Fails a task message :param task_id: :return: """ def ping(self) -> bool: """ Checks whether the broker connection is available :rtype: bool """ pass def info(self): """ Shows the broker type """ return self._info def set_stat(self, key: str, value: str, timeout: int): """ Saves a cluster statistic to the cache provider :type key: str :type value: str :type timeout: int """ if not self.cache: return key_list = self.cache.get(Conf.Q_STAT, []) if key not in key_list: key_list.append(key) self.cache.set(Conf.Q_STAT, key_list) return self.cache.set(key, value, timeout) def get_stat(self, key: str): """ Gets a cluster statistic from the cache provider :type key: str :return: a cluster Stat """ if not self.cache: return return self.cache.get(key) def get_stats(self, pattern: str) -> Optional[list]: """ Returns a list of all cluster stats from the cache provider :type pattern: str :return: a list of Stats """ if not self.cache: return key_list = self.cache.get(Conf.Q_STAT) if not key_list or len(key_list) == 0: return [] stats = [] for key in key_list: stat = self.cache.get(key) if stat: stats.append(stat) else: key_list.remove(key) self.cache.set(Conf.Q_STAT, key_list) return stats @staticmethod def get_cache(): """ Gets the current cache provider :return: a cache provider """ try: return caches[Conf.CACHE] except InvalidCacheBackendError: return None @staticmethod def get_connection(list_key: str = None): """ Gets a connection to the broker :param list_key: Optional queue name :return: a broker connection """ return 0 def get_broker(list_key: str = None) -> Broker: """ Gets the configured broker type :param list_key: optional queue name :type list_key: str :return: a broker instance """ list_key = list_key or Conf.CLUSTER_NAME # custom if Conf.BROKER_CLASS: module, func = Conf.BROKER_CLASS.rsplit(".", 1) m = importlib.import_module(module) broker = getattr(m, func) return broker(list_key=list_key) # Iron MQ elif Conf.IRON_MQ: from django_q.brokers import ironmq return ironmq.IronMQBroker(list_key=list_key) # SQS elif isinstance(Conf.SQS, dict): from django_q.brokers import aws_sqs return aws_sqs.Sqs(list_key=list_key) # ORM elif Conf.ORM: from django_q.brokers import orm return orm.ORM(list_key=list_key) # Mongo elif Conf.MONGO: from django_q.brokers import mongo return mongo.Mongo(list_key=list_key) # default to redis else: from django_q.brokers import redis_broker return redis_broker.Redis(list_key=list_key) django-q2-1.7.4/django_q/brokers/aws_sqs.py000066400000000000000000000063771471170400300206020ustar00rootroot00000000000000import copy from boto3 import Session from botocore.client import ClientError from django_q.brokers import Broker from django_q.conf import Conf QUEUE_DOES_NOT_EXIST = "AWS.SimpleQueueService.NonExistentQueue" class Sqs(Broker): def __init__(self, list_key: str = None): self.sqs = None super(Sqs, self).__init__(list_key) self.queue = self.get_queue() def __setstate__(self, state): super(Sqs, self).__setstate__(state) self.sqs = None self.queue = self.get_queue() def enqueue(self, task): response = self.queue.send_message(MessageBody=task) return response.get("MessageId") def dequeue(self): # sqs supports max 10 messages in bulk if Conf.BULK > 10: Conf.BULK = 10 params = {"MaxNumberOfMessages": Conf.BULK, "VisibilityTimeout": Conf.RETRY} # sqs long polling sqs_config = Conf.SQS if "receive_message_wait_time_seconds" in sqs_config: wait_time_second = sqs_config.get("receive_message_wait_time_seconds", 20) # validation of parameter if not isinstance(wait_time_second, int): raise ValueError("receive_message_wait_time_seconds should be int") if wait_time_second > 20: raise ValueError( "receive_message_wait_time_seconds is invalid. Reason: Must be >= 0" " and <= 20" ) params.update({"WaitTimeSeconds": wait_time_second}) tasks = self.queue.receive_messages(**params) if tasks: return [(t.receipt_handle, t.body) for t in tasks] def acknowledge(self, task_id): return self.delete(task_id) def queue_size(self) -> int: return int(self.queue.attributes["ApproximateNumberOfMessages"]) def lock_size(self) -> int: return int(self.queue.attributes["ApproximateNumberOfMessagesNotVisible"]) def delete(self, task_id): message = self.sqs.Message(self.queue.url, task_id) message.delete() def fail(self, task_id): self.delete(task_id) def delete_queue(self): self.queue.delete() def purge_queue(self): self.queue.purge() def ping(self) -> bool: return "sqs" in self.connection.get_available_resources() def info(self) -> str: return "AWS SQS" @staticmethod def get_connection(list_key: str = None) -> Session: config_cloned = copy.deepcopy(Conf.SQS) if "aws_region" in config_cloned: config_cloned["region_name"] = config_cloned["aws_region"] del config_cloned["aws_region"] if "receive_message_wait_time_seconds" in config_cloned: del config_cloned["receive_message_wait_time_seconds"] return Session(**config_cloned) def get_queue(self): self.sqs = self.connection.resource("sqs") try: # try to return an existing queue by name. If the queue does not # exist try to create it. return self.sqs.get_queue_by_name(QueueName=self.list_key) except ClientError as exp: if exp.response["Error"]["Code"] != QUEUE_DOES_NOT_EXIST: raise exp return self.sqs.create_queue(QueueName=self.list_key) django-q2-1.7.4/django_q/brokers/ironmq.py000066400000000000000000000025741471170400300204220ustar00rootroot00000000000000from iron_mq import IronMQ, Queue from requests.exceptions import HTTPError from django_q.brokers import Broker from django_q.conf import Conf class IronMQBroker(Broker): def enqueue(self, task): return self.connection.post(task)["ids"][0] def dequeue(self): timeout = Conf.RETRY or None tasks = self.connection.get(timeout=timeout, wait=1, max=Conf.BULK)["messages"] if tasks: return [(t["id"], t["body"]) for t in tasks] def ping(self) -> bool: return self.connection.name == self.list_key def info(self) -> str: return "IronMQ" def queue_size(self): return self.connection.size() def delete_queue(self): try: return self.connection.delete_queue()["msg"] except HTTPError: return False def purge_queue(self): return self.connection.clear() def delete(self, task_id): try: return self.connection.delete(task_id)["msg"] except HTTPError: return False def fail(self, task_id): self.delete(task_id) def acknowledge(self, task_id): return self.delete(task_id) @staticmethod def get_connection(list_key: str = None) -> Queue: list_key = list_key or Conf.CLUSTER_NAME ironmq = IronMQ(name=None, **Conf.IRON_MQ) return ironmq.queue(queue_name=list_key) django-q2-1.7.4/django_q/brokers/mongo.py000066400000000000000000000044271471170400300202330ustar00rootroot00000000000000from datetime import timedelta from time import sleep from bson import ObjectId from django.utils import timezone from pymongo import MongoClient from pymongo.errors import ConfigurationError from django_q.brokers import Broker from django_q.conf import Conf def _timeout(): return timezone.now() - timedelta(seconds=Conf.RETRY) class Mongo(Broker): def __init__(self, list_key: str = None): super(Mongo, self).__init__(list_key) self.collection = self.get_collection() def __setstate__(self, state): super(Mongo, self).__setstate__(state) self.collection = self.get_collection() @staticmethod def get_connection(list_key: str = None) -> MongoClient: return MongoClient(**Conf.MONGO) def get_collection(self): if not Conf.MONGO_DB: try: Conf.MONGO_DB = self.connection.get_default_database().name except ConfigurationError: Conf.MONGO_DB = "django-q" return self.connection[Conf.MONGO_DB][self.list_key] def queue_size(self): return self.collection.count_documents({"lock": {"$lte": _timeout()}}) def lock_size(self): return self.collection.count_documents({"lock": {"$gt": _timeout()}}) def purge_queue(self): return self.delete_queue() def ping(self) -> bool: return self.info is not None def info(self) -> str: if not self._info: self._info = f"MongoDB {self.connection.server_info()['version']}" return self._info def fail(self, task_id): self.delete(task_id) def enqueue(self, task): inserted_id = self.collection.insert_one( {"payload": task, "lock": _timeout()} ).inserted_id return str(inserted_id) def dequeue(self): task = self.collection.find_one_and_update( {"lock": {"$lte": _timeout()}}, {"$set": {"lock": timezone.now()}} ) if task: return [(str(task["_id"]), task["payload"])] # empty queue, spare the cpu sleep(Conf.POLL) def delete_queue(self): return self.collection.drop() def delete(self, task_id): self.collection.delete_one({"_id": ObjectId(task_id)}) def acknowledge(self, task_id): return self.delete(task_id) django-q2-1.7.4/django_q/brokers/orm.py000066400000000000000000000053771471170400300177160ustar00rootroot00000000000000from datetime import timedelta from time import sleep from django import db from django.db import transaction from django.utils import timezone from django_q.brokers import Broker from django_q.conf import Conf, logger from django_q.models import OrmQ def _timeout(): return timezone.now() + timedelta(seconds=Conf.RETRY) class ORM(Broker): @staticmethod def get_connection(list_key: str = None): if transaction.get_autocommit( using=Conf.ORM ): # Only True when not in an atomic block # Make sure stale connections in the broker thread are explicitly # closed before attempting DB access. # logger.debug("Broker thread calling close_old_connections") db.close_old_connections() else: logger.debug("Broker in an atomic transaction") return OrmQ.objects.using(Conf.ORM) def queue_size(self) -> int: return ( self.get_connection() .filter(key=self.list_key, lock__lte=timezone.now()) .count() ) def lock_size(self) -> int: return ( self.get_connection() .filter(key=self.list_key, lock__gt=timezone.now()) .count() ) def purge_queue(self): return self.get_connection().filter(key=self.list_key).delete() def ping(self) -> bool: return True def info(self) -> str: if not self._info: self._info = f"ORM {Conf.ORM}" return self._info def fail(self, task_id): self.delete(task_id) def enqueue(self, task): # list_key might be null (e.g. in a test setup) but OrmQ.key has not-null constraint package = self.get_connection().create( key=self.list_key or Conf.CLUSTER_NAME, payload=task, lock=timezone.now() ) return package.pk def dequeue(self): tasks = self.get_connection().filter( key=self.list_key, lock__lt=timezone.now() )[ 0 : Conf.BULK # noqa: E203 ] if tasks: task_list = [] for task in tasks: if ( self.get_connection() .filter(id=task.id, lock=task.lock) .update(lock=_timeout()) ): task_list.append((task.pk, task.payload)) # else don't process, as another cluster has been faster than us on # that task return task_list # empty queue, spare the cpu sleep(Conf.POLL) def delete_queue(self): return self.purge_queue() def delete(self, task_id): self.get_connection().filter(pk=task_id).delete() def acknowledge(self, task_id): return self.delete(task_id) django-q2-1.7.4/django_q/brokers/redis_broker.py000066400000000000000000000036371471170400300215700ustar00rootroot00000000000000import redis from redis import Redis from django_q.brokers import Broker from django_q.conf import Conf, logger try: import django_redis except ImportError: django_redis = None class Redis(Broker): def __init__(self, list_key: str = None): list_key = list_key or Conf.CLUSTER_NAME super(Redis, self).__init__(list_key=f"django_q:{list_key}:q") def enqueue(self, task): return self.connection.rpush(self.list_key, task) def dequeue(self): task = self.connection.blpop(self.list_key, 1) if task: return [(None, task[1])] def queue_size(self): return self.connection.llen(self.list_key) def delete_queue(self): return self.connection.delete(self.list_key) def purge_queue(self): return self.connection.ltrim(self.list_key, 1, 0) def ping(self) -> bool: try: return self.connection.ping() except redis.ConnectionError as e: logger.error("Can not connect to Redis server.") raise e def info(self) -> str: if not self._info: info = self.connection.info("server") self._info = f"Redis {info['redis_version']}" return self._info def set_stat(self, key: str, value: str, timeout: int): self.connection.set(key, value, timeout) def get_stat(self, key: str): if self.connection.exists(key): return self.connection.get(key) def get_stats(self, pattern: str): keys = self.connection.keys(pattern=pattern) if keys: return self.connection.mget(keys) @staticmethod def get_connection(list_key: str = None) -> Redis: if django_redis and Conf.DJANGO_REDIS: return django_redis.get_redis_connection(Conf.DJANGO_REDIS) if isinstance(Conf.REDIS, str): return redis.from_url(Conf.REDIS) return redis.StrictRedis(**Conf.REDIS) django-q2-1.7.4/django_q/cluster.py000066400000000000000000000342301471170400300171210ustar00rootroot00000000000000# Standard import os import signal import socket import uuid from multiprocessing import Event, Process, Value, current_process from time import sleep # Django from django import core, db from django.apps.registry import apps try: apps.check_apps_ready() except core.exceptions.AppRegistryNotReady: import django django.setup() from django.utils import timezone from django.utils.translation import gettext_lazy as _ # Local from django_q.brokers import Broker, get_broker from django_q.conf import ( Conf, get_ppid, logger, prometheus_multiprocess, psutil, setproctitle, ) from django_q.humanhash import humanize from django_q.monitor import monitor from django_q.pusher import pusher from django_q.queues import Queue from django_q.scheduler import scheduler from django_q.status import Stat, Status from django_q.worker import worker class Cluster: def __init__(self, broker: Broker = None): # Cluster do not need an init or default broker except for testing, # The sentinel will create a broker for cluster and utilize ALT_CLUSTERS config in Conf. self.broker = broker # DON'T USE get_broker() to set a default broker here. self.sentinel = None self.stop_event = None self.start_event = None self.pid = current_process().pid self.cluster_id = uuid.uuid4() self.host = socket.gethostname() self.timeout = None signal.signal(signal.SIGTERM, self.sig_handler) signal.signal(signal.SIGINT, self.sig_handler) def start(self) -> int: if setproctitle: setproctitle.setproctitle(f"qcluster {current_process().name} {self.name}") # Start Sentinel self.stop_event = Event() self.start_event = Event() self.sentinel = Process( target=Sentinel, name=f"Process-{uuid.uuid4().hex}", args=( self.stop_event, self.start_event, self.cluster_id, self.broker, self.timeout, ), ) self.sentinel.start() logger.info(_("Q Cluster %(name)s starting.") % {"name": self.name}) while not self.start_event.is_set(): sleep(0.1) return self.pid def stop(self) -> bool: if not self.sentinel.is_alive(): return False logger.info(_("Q Cluster %(name)s stopping.") % {"name": self.name}) self.stop_event.set() self.sentinel.join() logger.info(_("Q Cluster %(name)s has stopped.") % {"name": self.name}) self.start_event = None self.stop_event = None return True def sig_handler(self, signum, frame): logger.debug( _("%(name)s got signal %(signal)s") % { "name": current_process().name, "signal": Conf.SIGNAL_NAMES.get(signum, "UNKNOWN"), } ) self.stop() @property def stat(self) -> Status: if self.sentinel: return Stat.get(pid=self.pid, cluster_id=self.cluster_id) return Status(pid=self.pid, cluster_id=self.cluster_id) @property def name(self) -> str: return humanize(self.cluster_id.hex) @property def is_starting(self) -> bool: return self.stop_event and self.start_event and not self.start_event.is_set() @property def is_running(self) -> bool: return self.stop_event and self.start_event and self.start_event.is_set() @property def is_stopping(self) -> bool: return ( self.stop_event and self.start_event and self.start_event.is_set() and self.stop_event.is_set() ) @property def has_stopped(self) -> bool: return self.start_event is None and self.stop_event is None and self.sentinel class Sentinel: def __init__( self, stop_event, start_event, cluster_id, broker=None, timeout=None, start=True, ): # Make sure we catch signals for the pool signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGTERM, signal.SIG_DFL) self.pid = current_process().pid self.cluster_id = cluster_id self.parent_pid = get_ppid() self.name = current_process().name self.broker = broker or get_broker() self.reincarnations = 0 self.tob = timezone.now() self.stop_event = stop_event self.start_event = start_event self.pool_size = Conf.WORKERS self.pool = [] self.timeout = timeout or Conf.TIMEOUT self.task_queue = ( Queue(maxsize=Conf.QUEUE_LIMIT) if Conf.QUEUE_LIMIT else Queue() ) self.result_queue = Queue() self.event_out = Event() self.monitor = None self.pusher = None if start: self.start() def queue_name(self): # multi-queue: cluster name is (broker's) queue_name return self.broker.list_key if self.broker else "--" def start(self): self.broker.ping() self.spawn_cluster() self.guard() def status(self) -> str: if not self.start_event.is_set() and not self.stop_event.is_set(): return Conf.STARTING elif self.start_event.is_set() and not self.stop_event.is_set(): if self.result_queue.empty() and self.task_queue.empty(): return Conf.IDLE return Conf.WORKING elif self.stop_event.is_set() and self.start_event.is_set(): if self.monitor.is_alive() or self.pusher.is_alive() or len(self.pool) > 0: return Conf.STOPPING return Conf.STOPPED def spawn_process(self, target, *args) -> Process: """ :type target: function or class """ p = Process(target=target, args=args, name=f"Process-{uuid.uuid4().hex}") p.daemon = True if target == worker: p.daemon = Conf.DAEMONIZE_WORKERS p.timer = args[2] self.pool.append(p) p.start() return p def spawn_pusher(self) -> Process: return self.spawn_process(pusher, self.task_queue, self.event_out, self.broker) def spawn_worker(self): self.spawn_process( worker, self.task_queue, self.result_queue, Value("f", -1), self.timeout ) def spawn_monitor(self) -> Process: return self.spawn_process(monitor, self.result_queue, self.broker) def reincarnate(self, process): """ :param process: the process to reincarnate :type process: Process or None """ # close connections before spawning new process if not Conf.SYNC: db.connections.close_all() if process == self.monitor: self.monitor = self.spawn_monitor() logger.critical( _("reincarnated monitor %(name)s after sudden death") % {"name": process.name} ) elif process == self.pusher: self.pusher = self.spawn_pusher() logger.critical( _("reincarnated pusher %(name)s after sudden death") % {"name": process.name} ) else: # check if prometheus is proper configurated prometheus_path = os.getenv( "PROMETHEUS_MULTIPROC_DIR", os.getenv("prometheus_multiproc_dir") ) if prometheus_multiprocess and prometheus_path: prometheus_multiprocess.mark_process_dead(process.pid) self.pool.remove(process) self.spawn_worker() if process.timer.value == 0: # only need to terminate on timeout, otherwise we risk destabilizing # the queues task_name = "" if psutil: try: process_name = psutil.Process(process.pid).name() name_splits = process_name.split(" ") task_name = ( name_splits[3] if len(name_splits) >= 4 and name_splits[2] == "processing" else "" ) except psutil.NoSuchProcess: pass process.terminate() if task_name: msg = _( "reincarnated worker %(name)s after timeout while processing task %(task_name)s" ) % {"name": process.name, "task_name": task_name} else: msg = _("reincarnated worker %(name)s after timeout") % { "name": process.name } logger.critical(msg) elif int(process.timer.value) == -2: logger.info(_("recycled worker %(name)s") % {"name": process.name}) else: logger.critical( _("reincarnated worker %(name)s after death") % {"name": process.name} ) self.reincarnations += 1 def spawn_cluster(self): self.pool = [] Stat(self).save() # close connections before spawning new process if not Conf.SYNC: db.connections.close_all() # spawn worker pool for __ in range(self.pool_size): self.spawn_worker() # spawn auxiliary self.monitor = self.spawn_monitor() self.pusher = self.spawn_pusher() # set worker cpu affinity if needed if psutil and Conf.CPU_AFFINITY: set_cpu_affinity(Conf.CPU_AFFINITY, [w.pid for w in self.pool]) def guard(self): logger.info( _("%(name)s guarding cluster %(cluster_name)s") % { "name": current_process().name, "cluster_name": humanize(self.cluster_id.hex) + f" [{self.queue_name()}]", } ) self.start_event.set() Stat(self).save() logger.info( _("Q Cluster %(cluster_name)s running.") % { "cluster_name": humanize(self.cluster_id.hex) + f" [{self.queue_name()}]" } ) counter = 0 cycle = Conf.GUARD_CYCLE # guard loop sleep in seconds # Guard loop. Runs at least once while not self.stop_event.is_set() or not counter: # Check Workers for p in self.pool: with p.timer.get_lock(): # Are you alive? if not p.is_alive() or p.timer.value == 0: self.reincarnate(p) continue # Decrement timer if work is being done if p.timer.value > 0: p.timer.value -= cycle # Check Monitor if not self.monitor.is_alive(): self.reincarnate(self.monitor) # Check Pusher if not self.pusher.is_alive(): self.reincarnate(self.pusher) # Call scheduler once a minute (or so) counter += cycle if counter >= 30 and Conf.SCHEDULER: counter = 0 scheduler(broker=self.broker) # Save current status Stat(self).save() sleep(cycle) self.stop() def stop(self): Stat(self).save() name = current_process().name logger.info(_("%(name)s stopping cluster processes") % {"name": name}) # Stopping pusher self.event_out.set() # Wait for it to stop while self.pusher.is_alive(): sleep(0.1) Stat(self).save() # Put poison pills in the queue for __ in range(len(self.pool)): self.task_queue.put("STOP") self.task_queue.close() # wait for the task queue to empty self.task_queue.join_thread() # Wait for all the workers to exit while len(self.pool): for p in self.pool: if not p.is_alive(): self.pool.remove(p) sleep(0.1) Stat(self).save() # Finally stop the monitor self.result_queue.put("STOP") self.result_queue.close() # Wait for the result queue to empty self.result_queue.join_thread() logger.info(_("%(name)s waiting for the monitor.") % {"name": name}) # Wait for everything to close or time out count = 0 if not self.timeout: self.timeout = 30 while self.status() == Conf.STOPPING and count < self.timeout * 10: sleep(0.1) Stat(self).save() count += 1 # Final status Stat(self).save() def set_cpu_affinity(n: int, process_ids: list, actual: bool = not Conf.TESTING): """ Sets the cpu affinity for the supplied processes. Requires the optional psutil module. :param int n: affinity :param list process_ids: a list of pids :param bool actual: Test workaround for Travis not supporting cpu affinity """ # check if we have the psutil module if not psutil: logger.warning(_("Skipping cpu affinity because psutil was not found.")) return # check if the platform supports cpu_affinity if actual and not hasattr(psutil.Process(process_ids[0]), "cpu_affinity"): logger.warning( _("Faking cpu affinity because it is not supported on this platform") ) actual = False # get the available processors cpu_list = list(range(psutil.cpu_count())) # affinities of 0 or gte cpu_count, equals to no affinity if not n or n >= len(cpu_list): return # spread the workers over the available processors. index = 0 for pid in process_ids: affinity = [] for k in range(n): if index == len(cpu_list): index = 0 affinity.append(cpu_list[index]) index += 1 if psutil.pid_exists(pid): p = psutil.Process(pid) if actual: p.cpu_affinity(affinity) logger.info( _("%(pid)s will use cpu %(affinity)s") % {"pid": pid, "affinity": affinity} ) django-q2-1.7.4/django_q/conf.py000066400000000000000000000226071471170400300163720ustar00rootroot00000000000000import logging import os import sys from copy import deepcopy from multiprocessing import cpu_count from signal import signal from warnings import warn from django.conf import settings from django.utils.translation import gettext_lazy as _ from django_q.queues import Queue # The "selectable" entry points were introduced in importlib_metadata 3.6 and Python 3.10. if sys.version_info < (3, 10): from importlib_metadata import entry_points else: from importlib.metadata import entry_points # optional try: import psutil except ImportError: psutil = None try: from croniter import croniter except ImportError: croniter = None try: import resource except ModuleNotFoundError: resource = None try: import setproctitle except ModuleNotFoundError: setproctitle = None try: from prometheus_client import multiprocess as prometheus_multiprocess except ModuleNotFoundError: prometheus_multiprocess = None class Conf: """ Configuration class """ try: conf = settings.Q_CLUSTER.copy() except AttributeError: conf = {} _Q_CLUSTER_NAME = os.getenv("Q_CLUSTER_NAME") if ( _Q_CLUSTER_NAME and _Q_CLUSTER_NAME != conf.get("name") and _Q_CLUSTER_NAME != conf.get("cluster_name") ): conf["cluster_name"] = _Q_CLUSTER_NAME alt_conf = conf.pop("ALT_CLUSTERS") if isinstance(alt_conf, dict): alt_conf = alt_conf.get(_Q_CLUSTER_NAME) if isinstance(alt_conf, dict): alt_conf.pop("name", None) alt_conf.pop("cluster_name", None) conf.update(alt_conf) # Redis server configuration . Follows standard redis keywords REDIS = conf.get("redis", {}) # Support for Django-Redis connections DJANGO_REDIS = conf.get("django_redis", None) # IronMQ broker IRON_MQ = conf.get("iron_mq", None) # SQS broker SQS = conf.get("sqs", None) # ORM broker ORM = conf.get("orm", None) # Custom broker class BROKER_CLASS = conf.get("broker_class", None) # Database Poll POLL = conf.get("poll", 0.2) # MongoDB broker MONGO = conf.get("mongo", None) MONGO_DB = conf.get("mongo_db", None) # Name of the cluster or site. For when you run multiple sites on one redis server # It's also the `salt` for signing OrmQ, and part of the Redis stats caching key # For all clusters in one site, PREFIX should be the same value to be able to decrypt payloads PREFIX = conf.get("name", "default") # Support alternative cluster name to use multiple queues in one site. # cluster name and queue name are interchangeable, same thing. CLUSTER_NAME = conf.get("cluster_name", PREFIX) # Log output level LOG_LEVEL = conf.get("log_level", "INFO") # Maximum number of successful tasks kept in the database. 0 saves everything. # -1 saves none # Failures are always saved SAVE_LIMIT = conf.get("save_limit", 250) # save-limit can be set per Task's "group" or "name" or "func" SAVE_LIMIT_PER = conf.get("save_limit_per", None) # Verify SAVE_LIMIT_PER is valid if SAVE_LIMIT_PER not in ["group", "name", "func", None]: warn( _( "SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: " "'group', 'name', 'func' and None. Default is None." ) % {"option": SAVE_LIMIT_PER} ) # Guard loop sleep in seconds. Should be between 0 and 60 seconds. GUARD_CYCLE = conf.get("guard_cycle", 0.5) # Disable the scheduler SCHEDULER = conf.get("scheduler", True) # Number of workers in the pool. Default is cpu count if implemented, otherwise 4. WORKERS = conf.get("workers", False) if not WORKERS: try: WORKERS = cpu_count() # in rare cases this might fail except NotImplementedError: # try psutil if psutil: WORKERS = psutil.cpu_count() or 4 else: # sensible default WORKERS = 4 # Option to undaemonize the workers and allow them to spawn child processes DAEMONIZE_WORKERS = conf.get("daemonize_workers", True) # Maximum number of tasks that each cluster can work on QUEUE_LIMIT = conf.get("queue_limit", int(WORKERS) ** 2) # Sets compression of redis packages COMPRESSED = conf.get("compress", False) # Number of tasks each worker can handle before it gets recycled. # Useful for releasing memory RECYCLE = conf.get("recycle", 500) # The maximum resident set size in kilobytes before a worker will recycle. # Useful for limiting memory usage. Not available on all platforms MAX_RSS = conf.get("max_rss", None) # Number of seconds to wait for a worker to finish. TIMEOUT = conf.get("timeout", None) # Whether to acknowledge unsuccessful tasks. # This causes failed tasks to be considered delivered, thereby removing them from # the task queue. Defaults to False. ACK_FAILURES = conf.get("ack_failures", False) # Number of seconds to wait for acknowledgement before retrying a task # Only works with brokers that guarantee delivery. Defaults to 60 seconds. RETRY = conf.get("retry", 60) # Verify if retry and timeout settings are correct if not TIMEOUT or (TIMEOUT > RETRY): warn( "Retry and timeout are misconfigured. Set retry larger than timeout," "failure to do so will cause the tasks to be retriggered before completion." "See https://django-q2.readthedocs.io/en/master/configure.html#retry " "for details." ) # Sets the amount of tasks the cluster will try to pop off the broker. # If it supports bulk gets. BULK = conf.get("bulk", 1) # The Django Admin label for this app LABEL = conf.get("label", "Django Q") # Sets the number of processors for each worker, defaults to all. CPU_AFFINITY = conf.get("cpu_affinity", 0) # Global sync option to for debugging SYNC = conf.get("sync", False) # The Django cache to use CACHE = conf.get("cache", "default") # Use the cache as result backend. Can be 'True' or an integer representing the # global cache timeout. # i.e 'cached: 60' , will make all results go the cache and expire in 60 seconds. CACHED = conf.get("cached", False) # If set to False the scheduler won't execute tasks in the past. # Instead it will run once and reschedule the next run in the future. Defaults to # True. CATCH_UP = conf.get("catch_up", True) # Use the secret key for package signing # Django itself should raise an error if it's not configured SECRET_KEY = settings.SECRET_KEY # The redis stats key Q_STAT = f"django_q:{PREFIX}:cluster" # Optional error reporting setup ERROR_REPORTER = conf.get("error_reporter", {}) # Optional attempt count. set to 0 for infinite attempts MAX_ATTEMPTS = conf.get("max_attempts", 0) # OSX doesn't implement qsize because of missing sem_getvalue() try: QSIZE = Queue().qsize() == 0 except (NotImplementedError, OSError): QSIZE = False # Getting the signal names SIGNAL_NAMES = dict( (getattr(signal, n), n) for n in dir(signal) if n.startswith("SIG") and "_" not in n ) # Translators: Cluster status descriptions STARTING = _("Starting") WORKING = _("Working") IDLE = _("Idle") STOPPED = _("Stopped") STOPPING = _("Stopping") # to manage workarounds during testing TESTING = conf.get("testing", False) # Timezone for next_run, overrules Django timezone TIME_ZONE = None if settings.USE_TZ: TIME_ZONE = conf.get("time_zone", settings.TIME_ZONE) # logger logger = logging.getLogger("django-q") # Set up standard logging handler in case there is none if not logger.hasHandlers(): logger.setLevel(level=getattr(logging, Conf.LOG_LEVEL)) logger.propagate = False formatter = logging.Formatter( fmt="%(asctime)s [Q] %(levelname)s %(message)s", datefmt="%H:%M:%S" ) handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) # Error Reporting Interface class ErrorReporter: # initialize with iterator of reporters (better name, targets?) def __init__(self, reporters): self.targets = [target for target in reporters] # report error to all configured targets def report(self): for t in self.targets: t.report() # error reporting setup (sentry or rollbar) if Conf.ERROR_REPORTER: error_conf = deepcopy(Conf.ERROR_REPORTER) try: reporters = [] # iterate through the configured error reporters, # and instantiate an ErrorReporter using the provided config for name, conf in error_conf.items(): for entry in entry_points(group="djangoq.errorreporters", name=name): Reporter = entry.load() reporters.append(Reporter(**conf)) error_reporter = ErrorReporter(reporters) except ImportError: error_reporter = None else: error_reporter = None # get parent pid compatibility def get_ppid(): if hasattr(os, "getppid"): return os.getppid() elif psutil: return psutil.Process(os.getpid()).ppid() else: raise OSError( "Your OS does not support `os.getppid`. Please install `psutil` as an " "alternative provider." ) django-q2-1.7.4/django_q/core_signing.py000066400000000000000000000053101471170400300201030ustar00rootroot00000000000000import datetime import time import zlib from django.core.signing import ( BadSignature, JSONSerializer, SignatureExpired, b64_decode, dumps, ) from django.core.signing import Signer as Sgnr from django.core.signing import TimestampSigner as TsS try: from django.core.signing import b62_decode except ImportError: # fallback for django 3.x from django.utils.baseconv import base62 b62_decode = base62.decode from django.utils.crypto import constant_time_compare from django.utils.encoding import force_bytes, force_str dumps = dumps """ The loads function is the same as the `django.core.signing.loads` function The difference is that `this` loads function calls `TimestampSigner` and `Signer` """ def loads( s, key=None, salt: str = "django.core.signing", serializer=JSONSerializer, max_age=None, ): """ Reverse of dumps(), raise BadSignature if signature fails. The serializer is expected to accept a bytestring. """ # TimestampSigner.unsign() returns str but base64 and zlib compression # operate on bytes. base64d = force_bytes( TimestampSigner(key=key, salt=salt).unsign(s, max_age=max_age) ) decompress = False if base64d[:1] == b".": # It's compressed; uncompress it first base64d = base64d[1:] decompress = True data = b64_decode(base64d) if decompress: data = zlib.decompress(data) return serializer().loads(data) class Signer(Sgnr): def unsign(self, signed_value): signed_value = force_str(signed_value) if self.sep not in signed_value: raise BadSignature('No "%s" found in value' % self.sep) value, sig = signed_value.rsplit(self.sep, 1) if constant_time_compare(sig, self.signature(value)): return force_str(value) raise BadSignature('Signature "%s" does not match' % sig) """ TimestampSigner is also the same as `django.core.signing.TimestampSigner` but is calling `this` Signer. """ class TimestampSigner(Signer, TsS): def unsign(self, value, max_age=None): """ Retrieve original value and check it wasn't signed more than max_age seconds ago. """ result = super(TimestampSigner, self).unsign(value) value, timestamp = result.rsplit(self.sep, 1) timestamp = b62_decode(timestamp) if max_age is not None: if isinstance(max_age, datetime.timedelta): max_age = max_age.total_seconds() # Check timestamp is not older than max_age age = time.time() - timestamp if age > max_age: raise SignatureExpired("Signature age %s > %s seconds" % (age, max_age)) return value django-q2-1.7.4/django_q/exceptions.py000066400000000000000000000003121471170400300176130ustar00rootroot00000000000000class TimeoutException(SystemExit): """ Exception for when a worker takes too long to complete a task Raising SystemExit will make sure the function terminates gracefully. """ pass django-q2-1.7.4/django_q/humanhash.py000066400000000000000000000161221471170400300174140ustar00rootroot00000000000000""" humanhash: Human-readable representations of digests. The simplest ways to use this module are the :func:`humanize` and :func:`uuid` functions. For tighter control over the output, see :class:`HumanHasher`. """ import operator import uuid as uuidlib from argparse import ArgumentError from functools import reduce DEFAULT_WORDLIST = ( "ack", "alabama", "alanine", "alaska", "alpha", "angel", "apart", "april", "arizona", "arkansas", "artist", "asparagus", "aspen", "august", "autumn", "avocado", "bacon", "bakerloo", "batman", "beer", "berlin", "beryllium", "black", "blossom", "blue", "bluebird", "bravo", "bulldog", "burger", "butter", "california", "carbon", "cardinal", "carolina", "carpet", "cat", "ceiling", "charlie", "chicken", "coffee", "cola", "cold", "colorado", "comet", "connecticut", "crazy", "cup", "dakota", "december", "delaware", "delta", "diet", "don", "double", "early", "earth", "east", "echo", "edward", "eight", "eighteen", "eleven", "emma", "enemy", "equal", "failed", "fanta", "fifteen", "fillet", "finch", "fish", "five", "fix", "floor", "florida", "football", "four", "fourteen", "foxtrot", "freddie", "friend", "fruit", "gee", "georgia", "glucose", "golf", "green", "grey", "hamper", "happy", "harry", "hawaii", "helium", "high", "hot", "hotel", "hydrogen", "idaho", "illinois", "india", "indigo", "ink", "iowa", "island", "item", "jersey", "jig", "johnny", "juliet", "july", "jupiter", "kansas", "kentucky", "kilo", "king", "kitten", "lactose", "lake", "lamp", "lemon", "leopard", "lima", "lion", "lithium", "london", "louisiana", "low", "magazine", "magnesium", "maine", "mango", "march", "mars", "maryland", "massachusetts", "may", "mexico", "michigan", "mike", "minnesota", "mirror", "mississippi", "missouri", "mobile", "mockingbird", "monkey", "montana", "moon", "mountain", "muppet", "music", "nebraska", "neptune", "network", "nevada", "nine", "nineteen", "nitrogen", "north", "november", "nuts", "october", "ohio", "oklahoma", "one", "orange", "oranges", "oregon", "oscar", "oven", "oxygen", "papa", "paris", "pasta", "pennsylvania", "pip", "pizza", "pluto", "potato", "princess", "purple", "quebec", "queen", "quiet", "red", "river", "robert", "robin", "romeo", "rugby", "sad", "salami", "saturn", "september", "seven", "seventeen", "shade", "sierra", "single", "sink", "six", "sixteen", "skylark", "snake", "social", "sodium", "solar", "south", "spaghetti", "speaker", "spring", "stairway", "steak", "stream", "summer", "sweet", "table", "tango", "ten", "tennessee", "tennis", "texas", "thirteen", "three", "timing", "triple", "twelve", "twenty", "two", "uncle", "undress", "uniform", "uranus", "utah", "vegan", "venus", "vermont", "victor", "video", "violet", "virginia", "washington", "west", "whiskey", "white", "william", "winner", "winter", "wisconsin", "wolfram", "wyoming", "xray", "yankee", "yellow", "zebra", "zulu", ) class HumanHasher: """ Transforms hex digests to human-readable strings. The format of these strings will look something like: `victor-bacon-zulu-lima`. The output is obtained by compressing the input digest to a fixed number of bytes, then mapping those bytes to one of 256 words. A default wordlist is provided, but you can override this if you prefer. As long as you use the same wordlist, the output will be consistent (i.e. the same digest will always render the same representation). """ def __init__(self, wordlist=DEFAULT_WORDLIST): if len(wordlist) != 256: raise ArgumentError("Wordlist must have exactly 256 items") self.wordlist = wordlist def humanize(self, hexdigest, words=4, separator="-"): """ Humanize a given hexadecimal digest. Change the number of words output by specifying `words`. Change the word separator with `separator`. >>> digest = '60ad8d0d871b6095808297' >>> HumanHasher().humanize(digest) 'sodium-magnesium-nineteen-hydrogen' """ # Gets a list of byte values between 0-255. bytes = [ int(x, 16) for x in list(map("".join, list(zip(hexdigest[::2], hexdigest[1::2])))) ] # Compress an arbitrary number of bytes to `words`. compressed = self.compress(bytes, words) # Map the compressed byte values through the word list. return separator.join(self.wordlist[byte] for byte in compressed) @staticmethod def compress(bytes, target): """ Compress a list of byte values to a fixed target length. >>> bytes = [96, 173, 141, 13, 135, 27, 96, 149, 128, 130, 151] >>> HumanHasher.compress(bytes, 4) [205, 128, 156, 96] Attempting to compress a smaller number of bytes to a larger number is an error: >>> HumanHasher.compress(bytes, 15) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Fewer input bytes than requested output """ length = len(bytes) if target > length: raise ValueError("Fewer input bytes than requested output") # Split `bytes` into `target` segments. seg_size = length // target # fmt: off segments = [ bytes[i * seg_size : (i + 1) * seg_size] for i in range(target) # noqa: E203 E501 ] # fmt: on # Catch any left-over bytes in the last segment. segments[-1].extend(bytes[target * seg_size :]) # noqa: E203 E501 # Use a simple XOR checksum-like function for compression. def checksum(bytes): return reduce(operator.xor, bytes, 0) checksums = list(map(checksum, segments)) return checksums def uuid(self, **params): """ Generate a UUID with a human-readable representation. Returns `(human_repr, full_digest)`. Accepts the same keyword arguments as :meth:`humanize` (they'll be passed straight through). """ digest = str(uuidlib.uuid4()).replace("-", "") return self.humanize(digest, **params), digest DEFAULT_HASHER = HumanHasher() uuid = DEFAULT_HASHER.uuid humanize = DEFAULT_HASHER.humanize django-q2-1.7.4/django_q/locale/000077500000000000000000000000001471170400300163235ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/de/000077500000000000000000000000001471170400300167135ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/de/LC_MESSAGES/000077500000000000000000000000001471170400300205005ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/de/LC_MESSAGES/django.po000066400000000000000000000262341471170400300223110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-01-26 01:38+0000\n" "PO-Revision-Date: 2018-08-05 18:28+0200\n" "Last-Translator: Jonas Winkler\n" "Language-Team: \n" "Language: de-DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.1.1\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:43 msgid "Resubmit selected tasks to queue" msgstr "Ausgewählte Aufgaben erneut ausfÃŧhren" #: admin.py:107 models.py:293 #, fuzzy #| msgid "Success" msgid "success" msgstr "erfolg" #: admin.py:119 models.py:295 msgid "last_run" msgstr "" #: cluster.py:79 #, python-format msgid "Q Cluster %(name)s starting." msgstr "Q-Cluster %(name)s wird gestartet." #: cluster.py:87 #, fuzzy, python-format #| msgid "Q Cluster-{} stopping." msgid "Q Cluster %(name)s stopping." msgstr "Q-Cluster {name} wird gestoppt." #: cluster.py:90 #, python-format msgid "Q Cluster %(name)s has stopped." msgstr "Q-Cluster %(name)s wurde gestoppt." #: cluster.py:97 #, python-format msgid "%(name)s got signal %(signal)s" msgstr "%(name)s erhielt das Signal %(signal)s" #: cluster.py:224 #, python-format msgid "reincarnated monitor %(name)s after sudden death" msgstr "Monitor %(name)s wurde nach unerwartetem Absturz neu gestartet" #: cluster.py:230 #, python-format msgid "reincarnated pusher %(name)s after sudden death" msgstr "Pusher %(name)s wurde nach unerwartetem Absturz neu gestartet" #: cluster.py:250 #, fuzzy, python-format #| msgid "reincarnated worker %(name)s after timeout" msgid "" "reincarnated worker %(name)s after timeout while processing task " "%(task_name)s" msgstr "Worker %(name)s wurde nach ZeitÃŧberschreitung neu gestartet" #: cluster.py:255 #, python-format msgid "reincarnated worker %(name)s after timeout" msgstr "Worker %(name)s wurde nach ZeitÃŧberschreitung neu gestartet" #: cluster.py:260 #, python-format msgid "recycled worker %(name)s" msgstr "Worker %(name)s wurde wiederverwendet" #: cluster.py:263 #, python-format msgid "reincarnated worker %(name)s after death" msgstr "Worker %(name)s wurde nach unerwartetem Absturz neu gestartet" #: cluster.py:287 #, python-format msgid "%(name)s guarding cluster %(cluster_name)s" msgstr "%(name)s beschÃŧtzt das Cluster %(cluster_name)s" #: cluster.py:296 #, python-format msgid "Q Cluster %(cluster_name)s running." msgstr "Q-Cluster %(cluster_name)s läuft." #: cluster.py:332 #, python-format msgid "%(name)s stopping cluster processes" msgstr "%(name)s hält Cluster-Prozesse an" #: cluster.py:357 #, python-format msgid "%(name)s waiting for the monitor." msgstr "%(name)s wartet auf den Monitor." #: cluster.py:383 #, fuzzy, python-format #| msgid "%(process_name)s pushing tasks at %(id)s" msgid "%(name)s pushing tasks at %(id)s" msgstr "%(process_name)s verÃļffentlicht Aufagaben auf %(id)s" #: cluster.py:407 #, python-format msgid "queueing from %(list_key)s" msgstr "Einreihen von %(list_key)s" #: cluster.py:411 #, python-format msgid "%(name)s stopped pushing tasks" msgstr "%(name)s verÃļffentlicht keine Aufgaben mehr" #: cluster.py:426 #, python-format msgid "%(name)s monitoring at %(id)s" msgstr "%(name)s beobachtet auf %(id)s" #: cluster.py:445 #, python-format msgid "Processed '%(info_name)s' (%(task_name)s)" msgstr "[%(task_name)s] - '%(info_name)s' wurde verarbeitet" #: cluster.py:451 #, python-format msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s" msgstr "'%(info_name)s' (%(task_name)s) ist fehlgeschlagen - %(task_result)s" #: cluster.py:458 #, python-format msgid "%(name)s stopped monitoring results" msgstr "%(name)s Ãŧberwacht keine Ergebnisse mehr" #: cluster.py:474 #, python-format msgid "%(proc_name)s ready for work at %(id)s" msgstr "%(proc_name)s ist bereit fÃŧr Arbeit auf %(id)s" #: cluster.py:494 #, fuzzy, python-format #| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)" msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'" msgstr "%(proc_name)s verarbeitet '%(func_name)s' (%(task_name)s)" #: cluster.py:546 #, python-format msgid "%(proc_name)s stopped doing work" msgstr "%(proc_name)s hat die Arbeit beendet" #: cluster.py:751 #, python-format msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]" msgstr "" "%(process_name)s konnte keine Aufgabe von Zeitplan [%(schedule)s] erstellen" #: cluster.py:762 #, fuzzy, python-format #| msgid "%(process_name)s created a task from schedule [%(schedule)s]" msgid "" "%(process_name)s created task %(task_name)s from schedule [%(schedule)s]" msgstr "" "%(process_name)s hat eine Aufgabe des Zeitplans [%(schedule)s] erstellt" #: cluster.py:808 msgid "Skipping cpu affinity because psutil was not found." msgstr "Cpu-Affinität wird Ãŧbersprungen, da psutil nicht gefunden wurde." #: cluster.py:813 msgid "Faking cpu affinity because it is not supported on this platform" msgstr "" "Vortäuschen von CPU-Affinität, da diese auf dieser Plattform nicht " "unterstÃŧtzt wird" #: cluster.py:835 #, python-format msgid "%(pid)s will use cpu %(affinity)s" msgstr "%(pid)s wird CPU %(affinity)s benutzen" #: conf.py:90 #, python-format msgid "" "SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', " "'name', 'func' and None. Default is None." msgstr "" "SAVE_LIMIT_PER (%(option)s) ist keine gÃŧltige Option. Optionen sind: " "'group', 'name', 'func' und None. Standard ist None." #. Translators: Cluster status descriptions #: conf.py:207 msgid "Starting" msgstr "Wird gestartet" #: conf.py:208 msgid "Working" msgstr "Arbeitet" #: conf.py:209 msgid "Idle" msgstr "Leerlauf" #: conf.py:210 msgid "Stopped" msgstr "Gestoppt" #: conf.py:211 msgid "Stopping" msgstr "Wird gestoppt" #. Translators: help text for qcluster management command #: management/commands/qcluster.py:9 msgid "Starts a Django Q Cluster." msgstr "Startet ein Django-Q-Cluster." #. Translators: help text for qinfo management command #: management/commands/qinfo.py:11 msgid "General information over all clusters." msgstr "Allgemeine Informationen Ãŧber alle Cluster" #. Translators: help text for qmemory management command #: management/commands/qmemory.py:9 msgid "Monitors Q Cluster memory usage" msgstr "Überwacht die Speichernutzung von Q Cluster" #. Translators: help text for qmonitor management command #: management/commands/qmonitor.py:9 msgid "Monitors Q Cluster activity" msgstr "Q-Cluster aktiv Ãŧberwachen" #: models.py:125 msgid "Successful task" msgstr "Erfolgreiche Aufgabe" #: models.py:126 msgid "Successful tasks" msgstr "Erfolgreiche Aufgaben" #: models.py:141 msgid "Failed task" msgstr "Fehlgeschlagene Aufgabe" #: models.py:142 msgid "Failed tasks" msgstr "Fehlgeschlagene Aufgaben" #: models.py:150 models.py:234 msgid "Please install croniter to enable cron expressions" msgstr "Bitte installieren Sie croniter, um Cron-AusdrÃŧcke zu aktivieren" #: models.py:170 msgid "e.g. 1, 2, 'John'" msgstr "zum Beispiel 1, 2, 'John'" #: models.py:172 msgid "e.g. x=1, y=2, name='John'" msgstr "zum Beispiel x=1, y=2, name='John'" #: models.py:186 msgid "Once" msgstr "Einmal" #: models.py:187 msgid "Minutes" msgstr "Minuten" #: models.py:188 msgid "Hourly" msgstr "StÃŧndlich" #: models.py:189 msgid "Daily" msgstr "Täglich" #: models.py:190 msgid "Weekly" msgstr "WÃļchentlich" #: models.py:191 msgid "Biweekly" msgstr "ZweiwÃļchentlich" #: models.py:192 msgid "Monthly" msgstr "Monatlich" #: models.py:193 msgid "Bimonthly" msgstr "Zweimonatlich" #: models.py:194 msgid "Quarterly" msgstr "Vierteljährlich" #: models.py:195 msgid "Yearly" msgstr "Jährlich" #: models.py:196 msgid "Cron" msgstr "Cron" #: models.py:199 msgid "Schedule Type" msgstr "Zeitplan-Typ" #: models.py:202 msgid "Number of minutes for the Minutes type" msgstr "Anzahl Minuten fÃŧr den Typ 'Minuten'" #: models.py:205 msgid "Repeats" msgstr "Wiederholungen" #: models.py:205 msgid "n = n times, -1 = forever" msgstr "n = n mal, -1 = fÃŧr immer" #: models.py:208 msgid "Next Run" msgstr "Nächste AusfÃŧhrung" #: models.py:215 msgid "Cron expression" msgstr "Cron-Ausdruck" #: models.py:224 msgid "Name of kwarg to pass intended schedule date" msgstr "Name des zu passierenden Kwargs vorgesehenes Datum" #: models.py:299 msgid "Scheduled task" msgstr "Geplante Aufgabe" #: models.py:300 msgid "Scheduled tasks" msgstr "Geplante Aufgaben" #: models.py:326 msgid "Queued task" msgstr "Eingereihte Aufgabe" #: models.py:327 msgid "Queued tasks" msgstr "Eingereihte Aufgaben" #: monitor.py:64 monitor.py:348 msgid "Host" msgstr "Host" #: monitor.py:68 monitor.py:352 monitor.py:459 msgid "Id" msgstr "Id" #: monitor.py:72 msgid "State" msgstr "Status" #: monitor.py:76 msgid "Pool" msgstr "Pool" #: monitor.py:80 msgid "TQ" msgstr "TQ" #: monitor.py:84 msgid "RQ" msgstr "RQ" #: monitor.py:88 msgid "RC" msgstr "RC" #: monitor.py:92 msgid "Up" msgstr "Up" #: monitor.py:172 monitor.py:286 msgid "Queued" msgstr "Eingereiht" #: monitor.py:180 msgid "Success" msgstr "Erfolg" #: monitor.py:190 monitor.py:294 msgid "Failures" msgstr "Fehlschläge" #: monitor.py:201 monitor.py:498 msgid "[Press q to quit]" msgstr "[DrÃŧcken Sie q zum Beenden]" #: monitor.py:227 msgid "day" msgstr "Tag" #: monitor.py:248 msgid "second" msgstr "Sekunde" #: monitor.py:251 msgid "minute" msgstr "Minute" #: monitor.py:254 msgid "hour" msgstr "Stunde" #: monitor.py:263 #, python-format msgid "-- %(prefix)s %(version)s on %(info)s --" msgstr "-- %(prefix)s %(version)s auf %(info)s --" #: monitor.py:273 msgid "Clusters" msgstr "Cluster" #: monitor.py:277 msgid "Workers" msgstr "Arbeiter" #: monitor.py:281 msgid "Restarts" msgstr "Neustarts" #: monitor.py:290 msgid "Successes" msgstr "Erfolge" #: monitor.py:299 msgid "Schedules" msgstr "Zeitpläne" #: monitor.py:303 #, python-format msgid "Tasks/%(per)s" msgstr "Aufgaben/%(per)s" #: monitor.py:307 msgid "Avg time" msgstr "Durchschnittl. Zeit" #: monitor.py:357 msgid "Available (%)" msgstr "VerfÃŧgbar (%)" #: monitor.py:363 msgid "Available (MB)" msgstr "VerfÃŧgbar (MB)" #: monitor.py:368 msgid "Total (MB)" msgstr "Insgesamt (MB)" #: monitor.py:373 msgid "Sentinel (MB)" msgstr "Sentinel (MB)" #: monitor.py:379 msgid "Monitor (MB)" msgstr "Monitor (MB)" #: monitor.py:385 msgid "Workers (MB)" msgstr "Arbeiter (MB)" #: monitor.py:487 #, python-format msgid "Available lowest (): %(memory_percent)s ((at)s)" msgstr "Niedrigste verfÃŧgbar (): %(memory_percent)s ((at)s)" #: monitor.py:509 msgid "No clusters appear to be running." msgstr "Es scheinen keine Cluster zu laufen." #: signals.py:22 #, python-format msgid "malformed return hook '%(hook)s' for [%(name)s]" msgstr "UngÃŧltiger Return-Hook '%(hook)s' fÃŧr [%(name)s]" #: signals.py:30 #, python-format msgid "return hook %(hook)s failed on [%(name)s] because %(error)s" msgstr "Return-Hook %(hook)s fÃŧr [%(name)s] ist gescheitert: %(error)s" #, python-format #~ msgid "" #~ "Could not process '%(func_name)s'. Check the location of the function and " #~ "the args/kwargs." #~ msgstr "" #~ "Konnte '%(func_name)s' nicht verarbeiten. ÜberprÃŧfen Sie den Ort der " #~ "Funktion und die args/kwargs." django-q2-1.7.4/django_q/locale/fr/000077500000000000000000000000001471170400300167325ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/fr/LC_MESSAGES/000077500000000000000000000000001471170400300205175ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/fr/LC_MESSAGES/django.po000066400000000000000000000261761471170400300223350ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-01-26 01:38+0000\n" "PO-Revision-Date: 2018-08-05 18:28+0200\n" "Last-Translator: Thierry BOULOGNE \n" "Language-Team: \n" "Language: fr-FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.1.1\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:43 msgid "Resubmit selected tasks to queue" msgstr "Resoumettre les tÃĸches sÊlectionnÊes à la file d'attente" #: admin.py:107 models.py:293 #, fuzzy #| msgid "Success" msgid "success" msgstr "succès" #: admin.py:119 models.py:295 msgid "last_run" msgstr "" #: cluster.py:79 #, python-format msgid "Q Cluster %(name)s starting." msgstr "DÊmarrage de Q Cluster-%(name)s." #: cluster.py:87 #, python-format msgid "Q Cluster %(name)s stopping." msgstr "ArrÃĒt de Q Cluster-%(name)s." #: cluster.py:90 #, python-format msgid "Q Cluster %(name)s has stopped." msgstr "Q Cluster-%(name)s a ÊtÊ arrÃĒtÊ." #: cluster.py:97 #, python-format msgid "%(name)s got signal %(signal)s" msgstr "%(name)s à reçu le signal %(signal)s" #: cluster.py:224 #, python-format msgid "reincarnated monitor %(name)s after sudden death" msgstr "surveillant %(name)s rÊincarnÊ après un arrÃĒt intempestif" #: cluster.py:230 #, python-format msgid "reincarnated pusher %(name)s after sudden death" msgstr "rÊpartiteur %(name)s rÊincarnÊ après un arrÃĒt intempestif" #: cluster.py:250 #, python-format msgid "" "reincarnated worker %(name)s after timeout while processing task " "%(task_name)s" msgstr "" "processus %(name)s rÊincarnÊ, dÊlai de traitement dÊpassÊ pour la tÃĸche " "%(task_name)s" #: cluster.py:255 #, python-format msgid "reincarnated worker %(name)s after timeout" msgstr "processus %(name)s rÊincarnÊ, dÊlai de traitement dÊpassÊ" #: cluster.py:260 #, python-format msgid "recycled worker %(name)s" msgstr "processus recyclÊ %(name)s" #: cluster.py:263 #, python-format msgid "reincarnated worker %(name)s after death" msgstr "processus rÊintÊgrÊ %(name)s après arrÃĒt" #: cluster.py:287 #, python-format msgid "%(name)s guarding cluster %(cluster_name)s" msgstr "%(name)s surveillance du cluster à %(cluster_name)s" #: cluster.py:296 #, python-format msgid "Q Cluster %(cluster_name)s running." msgstr "DÊmarrage de Q Cluster-%(cluster_name)s." #: cluster.py:332 #, python-format msgid "%(name)s stopping cluster processes" msgstr "%(name)s arrÃĒt des processus du cluster" #: cluster.py:357 #, python-format msgid "%(name)s waiting for the monitor." msgstr "%(name)s en attente du surveillant." #: cluster.py:383 #, python-format msgid "%(name)s pushing tasks at %(id)s" msgstr "%(name)s rÊpartit les tÃĸches %(id)s" #: cluster.py:407 #, python-format msgid "queueing from %(list_key)s" msgstr "mise en file d'attente de %(list_key)s" #: cluster.py:411 #, python-format msgid "%(name)s stopped pushing tasks" msgstr "%(name)s a cessÊ de rÊpartir les tÃĸches" #: cluster.py:426 #, python-format msgid "%(name)s monitoring at %(id)s" msgstr "%(name)s surveille les rÊsultats %(id)s" #: cluster.py:445 #, python-format msgid "Processed '%(info_name)s' (%(task_name)s)" msgstr "traitÊ '%(info_name)s' (%(task_name)s)" #: cluster.py:451 #, python-format msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s" msgstr "ManquÊ '%(info_name)s' (%(task_name)s) - %(task_result)s" #: cluster.py:458 #, python-format msgid "%(name)s stopped monitoring results" msgstr "%(name)s a cessÊ de de surveiller les rÊsultats" #: cluster.py:474 #, python-format msgid "%(proc_name)s ready for work at %(id)s" msgstr "%(proc_name)s prÃĒt pour le travail à %(id)s" #: cluster.py:494 #, fuzzy, python-format msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'" msgstr "%(proc_name)s exÊcute %(task_name)s '%(func_name)s'" #: cluster.py:546 #, python-format msgid "%(proc_name)s stopped doing work" msgstr "%(proc_name)s a cessÊ de travailler" #: cluster.py:751 #, python-format msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]" msgstr "" "%(process_name)s Echec de la crÊation d'une tÃĸche à partir de Schedule " "[%(schedule)s]" #: cluster.py:762 #, python-format msgid "" "%(process_name)s created task %(task_name)s from schedule [%(schedule)s]" msgstr "" "%(process_name)s a crÊÊ la tÃĸche %(task_name)s à partir de Schedule " "[%(schedule)s]" #: cluster.py:808 msgid "Skipping cpu affinity because psutil was not found." msgstr "L'affinitÊ cpu ne sera pas dÊfinie car psutil n'a pas ÊtÊ trouvÊ." #: cluster.py:813 msgid "Faking cpu affinity because it is not supported on this platform" msgstr "" "Simulation de l'affinitÊ cpu parce qu'elle n'est pas supportÊe sur cette " "plateforme." #: cluster.py:835 #, python-format msgid "%(pid)s will use cpu %(affinity)s" msgstr "%(pid)s utilisera le CPU %(affinity)s" #: conf.py:90 #, python-format msgid "" "SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', " "'name', 'func' and None. Default is None." msgstr "" "SAVE_LIMIT_PER (%(option)s) n'est pas une option valide. Les options sont : " "'group', 'name', 'func' et None. La valeur par dÊfaut est None." #. Translators: Cluster status descriptions #: conf.py:207 msgid "Starting" msgstr "DÊmarrage" #: conf.py:208 msgid "Working" msgstr "Actif" #: conf.py:209 msgid "Idle" msgstr "En attente" #: conf.py:210 msgid "Stopped" msgstr "ArrÃĒtÊ" #: conf.py:211 msgid "Stopping" msgstr "En cours d’arrÃĒt" #. Translators: help text for qcluster management command #: management/commands/qcluster.py:9 msgid "Starts a Django Q Cluster." msgstr "DÊmarre un cluster Django Q." #. Translators: help text for qinfo management command #: management/commands/qinfo.py:11 msgid "General information over all clusters." msgstr "Informations gÊnÊrales sur tous les clusters." #. Translators: help text for qmemory management command #: management/commands/qmemory.py:9 #, fuzzy #| msgid "Monitors Q Cluster activity" msgid "Monitors Q Cluster memory usage" msgstr "Surveille l'utilisation mÊmoire du Q cluster" #. Translators: help text for qmonitor management command #: management/commands/qmonitor.py:9 msgid "Monitors Q Cluster activity" msgstr "Surveille l'activitÊ de Q cluster" #: models.py:125 msgid "Successful task" msgstr "TÃĸche rÊussie" #: models.py:126 msgid "Successful tasks" msgstr "TÃĸches rÊussies" #: models.py:141 msgid "Failed task" msgstr "TÃĸche ÊchouÊ" #: models.py:142 msgid "Failed tasks" msgstr "TÃĸches ÊchouÊes" #: models.py:150 models.py:234 msgid "Please install croniter to enable cron expressions" msgstr "Veuillez installer croniter pour activer les expressions cron." #: models.py:170 msgid "e.g. 1, 2, 'John'" msgstr "ex. 1, 2, ‘Jean’" #: models.py:172 msgid "e.g. x=1, y=2, name='John'" msgstr "p. ex. x = 1, y = 2, Nom = ‘Jean’" #: models.py:186 msgid "Once" msgstr "Une fois" #: models.py:187 msgid "Minutes" msgstr "Minutes" #: models.py:188 msgid "Hourly" msgstr "Toutes les heures" #: models.py:189 msgid "Daily" msgstr "Quotidien" #: models.py:190 msgid "Weekly" msgstr "Hebdomadaire" #: models.py:191 #, fuzzy #| msgid "Weekly" msgid "Biweekly" msgstr "Bihebdomadaire" #: models.py:192 msgid "Monthly" msgstr "Mensuel" #: models.py:193 #, fuzzy #| msgid "Monthly" msgid "Bimonthly" msgstr "Bimestriel" #: models.py:194 msgid "Quarterly" msgstr "Trimestriel" #: models.py:195 msgid "Yearly" msgstr "Annuel" #: models.py:196 msgid "Cron" msgstr "Cron" #: models.py:199 msgid "Schedule Type" msgstr "Type de plannification" #: models.py:202 msgid "Number of minutes for the Minutes type" msgstr "Nombre de minutes pour le type de minutes" #: models.py:205 msgid "Repeats" msgstr "RÊpÊter" #: models.py:205 msgid "n = n times, -1 = forever" msgstr "n = n fois,-1 = Toujours" #: models.py:208 msgid "Next Run" msgstr "Prochaine exÊcution" #: models.py:215 msgid "Cron expression" msgstr "Expression Cron" #: models.py:224 msgid "Name of kwarg to pass intended schedule date" msgstr "Nom du kwarg à passer Date prÊvue de l'horaire" #: models.py:299 msgid "Scheduled task" msgstr "TÃĸche planifiÊe" #: models.py:300 msgid "Scheduled tasks" msgstr "TÃĸches planifiÊes" #: models.py:326 msgid "Queued task" msgstr "TÃĸche en file d'attente" #: models.py:327 msgid "Queued tasks" msgstr "TÃĸches en file d'attente" #: monitor.py:64 monitor.py:348 msgid "Host" msgstr "Hôte" #: monitor.py:68 monitor.py:352 monitor.py:459 msgid "Id" msgstr "Id" #: monitor.py:72 msgid "State" msgstr "Statut" #: monitor.py:76 msgid "Pool" msgstr "Piscine" #: monitor.py:80 msgid "TQ" msgstr "TQ" #: monitor.py:84 msgid "RQ" msgstr "RQ" #: monitor.py:88 msgid "RC" msgstr "RC" #: monitor.py:92 msgid "Up" msgstr "Haut" #: monitor.py:172 monitor.py:286 msgid "Queued" msgstr "En file d'attente" #: monitor.py:180 msgid "Success" msgstr "Succès" #: monitor.py:190 monitor.py:294 msgid "Failures" msgstr "DÊfaillances" #: monitor.py:201 monitor.py:498 msgid "[Press q to quit]" msgstr "[appuyez sur q pour quitter]" #: monitor.py:227 msgid "day" msgstr "jour" #: monitor.py:248 msgid "second" msgstr "seconde" #: monitor.py:251 msgid "minute" msgstr "minute" #: monitor.py:254 msgid "hour" msgstr "heure" #: monitor.py:263 #, python-format msgid "-- %(prefix)s %(version)s on %(info)s --" msgstr "--%(prefix)s %(version)s sur %(info)s --" #: monitor.py:273 msgid "Clusters" msgstr "Grappes" #: monitor.py:277 msgid "Workers" msgstr "Processus" #: monitor.py:281 msgid "Restarts" msgstr "RedÊmarrages" #: monitor.py:290 msgid "Successes" msgstr "Succès" #: monitor.py:299 msgid "Schedules" msgstr "Planifications" #: monitor.py:303 #, python-format msgid "Tasks/%(per)s" msgstr "TÃĸches/%(per)s" #: monitor.py:307 msgid "Avg time" msgstr "Temps Moyen" #: monitor.py:357 msgid "Available (%)" msgstr "" #: monitor.py:363 msgid "Available (MB)" msgstr "Disponible sur (MB)" #: monitor.py:368 msgid "Total (MB)" msgstr "Total (MB)" #: monitor.py:373 msgid "Sentinel (MB)" msgstr "Sentinel (MB)" #: monitor.py:379 msgid "Monitor (MB)" msgstr "Monitor (MB)" #: monitor.py:385 #, fuzzy #| msgid "Workers" msgid "Workers (MB)" msgstr "Processus (MB)" #: monitor.py:487 #, python-format msgid "Available lowest (): %(memory_percent)s ((at)s)" msgstr "Disponible le plus bas () : %(memory_percent)s ((at)s)" #: monitor.py:509 msgid "No clusters appear to be running." msgstr "Aucun cluster ne semble ÃĒtre en cours d'exÊcution." #: signals.py:22 #, python-format msgid "malformed return hook '%(hook)s' for [%(name)s]" msgstr "hook de retour '%(hook)s' mal formÊ pour [%(name)s]" #: signals.py:30 #, python-format msgid "return hook %(hook)s failed on [%(name)s] because %(error)s" msgstr "hook de retour %(hook)s a ÊchouÊ sur [%(name)s] à cause de %(error)s" #, python-format #~ msgid "" #~ "Could not process '%(func_name)s'. Check the location of the function and " #~ "the args/kwargs." #~ msgstr "" #~ "Impossible de traiter '%(func_name)s'. VÊrifiez l'emplacement de la " #~ "fonction et les args/kwargs." django-q2-1.7.4/django_q/locale/tr/000077500000000000000000000000001471170400300167505ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/tr/LC_MESSAGES/000077500000000000000000000000001471170400300205355ustar00rootroot00000000000000django-q2-1.7.4/django_q/locale/tr/LC_MESSAGES/django.po000066400000000000000000000257511471170400300223510ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-01-26 01:38+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Ethem GÃŧner \n" "Language-Team: \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:43 msgid "Resubmit selected tasks to queue" msgstr "Seçili işleri kuyruğa tekrar gÃļnder" #: admin.py:107 models.py:293 #, fuzzy #| msgid "Success" msgid "success" msgstr "başarÄąlÄą olanlar" #: admin.py:119 models.py:295 msgid "last_run" msgstr "" #: cluster.py:79 #, python-format msgid "Q Cluster %(name)s starting." msgstr "Q Cluster %(name)s başlatÄąlÄąyor." #: cluster.py:87 #, python-format msgid "Q Cluster %(name)s stopping." msgstr "Q Cluster %(name)s durduruluyor." #: cluster.py:90 #, python-format msgid "Q Cluster %(name)s has stopped." msgstr "Q Cluster %(name)s durduruldu." #: cluster.py:97 #, python-format msgid "%(name)s got signal %(signal)s" msgstr "%(name)s, %(signal)s pid'inde izleniyor/monitoring yapÄąlÄąyor." #: cluster.py:224 #, python-format msgid "reincarnated monitor %(name)s after sudden death" msgstr "Monitor %(name)s ani ÃļlÃŧm sonrasÄą tekrar dirildi" #: cluster.py:230 #, python-format msgid "reincarnated pusher %(name)s after sudden death" msgstr "Pusher %(name)s ani ÃļlÃŧm sonrasÄą tekrar dirildi" #: cluster.py:250 #, fuzzy, python-format #| msgid "reincarnated worker %(name)s after timeout" msgid "" "reincarnated worker %(name)s after timeout while processing task " "%(task_name)s" msgstr "Worker %(name)s zaman aÅŸÄąmÄą sonrasÄą tekrar dirildi" #: cluster.py:255 #, python-format msgid "reincarnated worker %(name)s after timeout" msgstr "Worker %(name)s zaman aÅŸÄąmÄą sonrasÄą tekrar dirildi" #: cluster.py:260 #, python-format msgid "recycled worker %(name)s" msgstr "Worker %(name)s geri dÃļndÃŧrÃŧldÃŧ" #: cluster.py:263 #, python-format msgid "reincarnated worker %(name)s after death" msgstr "Worker %(name)s ani ÃļlÃŧm sonrasÄą tekrar dirildi" #: cluster.py:287 #, python-format msgid "%(name)s guarding cluster %(cluster_name)s" msgstr "%(name)s, %(cluster_name)s cluster'ÄąnÄą koruyor" #: cluster.py:296 #, python-format msgid "Q Cluster %(cluster_name)s running." msgstr "Q Cluster %(cluster_name)s başlatÄąlÄąyor." #: cluster.py:332 #, python-format msgid "%(name)s stopping cluster processes" msgstr "Cluster %(name)s işlemleri durduruluyor." #: cluster.py:357 #, python-format msgid "%(name)s waiting for the monitor." msgstr "%(name)s monitor için bekliyor." #: cluster.py:383 #, fuzzy, python-format #| msgid "%(process_name)s pushing tasks at %(id)s" msgid "%(name)s pushing tasks at %(id)s" msgstr "%(process_name)s, işleri %(id)s pid'ine gÃļnderiyor." #: cluster.py:407 #, python-format msgid "queueing from %(list_key)s" msgstr "" #: cluster.py:411 #, python-format msgid "%(name)s stopped pushing tasks" msgstr "%(name)s işleri gÃļndermeyi durdurdu" #: cluster.py:426 #, python-format msgid "%(name)s monitoring at %(id)s" msgstr "%(name)s, %(id)s pid'inde izleniyor/monitoring yapÄąlÄąyor." #: cluster.py:445 #, python-format msgid "Processed '%(info_name)s' (%(task_name)s)" msgstr "[%(task_name)s] - '%(info_name)s işlendi." #: cluster.py:451 #, python-format msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s" msgstr "[%(task_name)s] - '%(info_name)s' - %(task_result)s başarÄąsÄąz oldu" #: cluster.py:458 #, python-format msgid "%(name)s stopped monitoring results" msgstr "%(name)s sonuçlarÄą gÃļstermeyi bÄąraktÄą" #: cluster.py:474 #, python-format msgid "%(proc_name)s ready for work at %(id)s" msgstr "%(proc_name)s, %(id)s pid'inde çalÄąÅŸmaya hazÄąr" #: cluster.py:494 #, fuzzy, python-format #| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)" msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'" msgstr "%(proc_name)s, '%(func_name)s' [%(task_name)s] işlerini işiyor" #: cluster.py:546 #, python-format msgid "%(proc_name)s stopped doing work" msgstr "%(proc_name)s çalÄąÅŸmayÄą bÄąraktÄą" #: cluster.py:751 #, python-format msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]" msgstr "%(process_name)s programdan bir gÃļrev oluşturamadÄą [%(schedule)s]" #: cluster.py:762 #, fuzzy, python-format #| msgid "%(process_name)s created a task from schedule [%(schedule)s]" msgid "" "%(process_name)s created task %(task_name)s from schedule [%(schedule)s]" msgstr "%(process_name)s programdan bir gÃļrev oluşturamadÄą [%(schedule)s]" #: cluster.py:808 msgid "Skipping cpu affinity because psutil was not found." msgstr "Psutil bulunamadığı için cpu benzeşimi atlanÄąyor." #: cluster.py:813 msgid "Faking cpu affinity because it is not supported on this platform" msgstr "Bu platformda desteklenmediği için sahte cpu benzeşimi" #: cluster.py:835 #, python-format msgid "%(pid)s will use cpu %(affinity)s" msgstr "%(pid)s cpu %(affinity)s kullanacaktÄąr" #: conf.py:90 #, python-format msgid "" "SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', " "'name', 'func' and None. Default is None." msgstr "" "SAVE_LIMIT_PER (%(option)s) geçerli bir seçenek değil. Seçenekler şunlardÄąr: " "'group', 'name', 'func' ve None. VarsayÄąlan değer None'dÄąr." #. Translators: Cluster status descriptions #: conf.py:207 msgid "Starting" msgstr "BaşlÄąyor" #: conf.py:208 msgid "Working" msgstr "ÇalÄąÅŸÄąyor" #: conf.py:209 msgid "Idle" msgstr "Boşta" #: conf.py:210 msgid "Stopped" msgstr "Durdu" #: conf.py:211 msgid "Stopping" msgstr "Durduruluyor" #. Translators: help text for qcluster management command #: management/commands/qcluster.py:9 msgid "Starts a Django Q Cluster." msgstr "Bir Django Q Cluster çalÄąÅŸtÄąrÄąr." #. Translators: help text for qinfo management command #: management/commands/qinfo.py:11 msgid "General information over all clusters." msgstr "TÃŧm cluster'lar için genel bilgiler." #. Translators: help text for qmemory management command #: management/commands/qmemory.py:9 msgid "Monitors Q Cluster memory usage" msgstr "Q Cluster'Äąn bellek kullanÄąmÄąnÄą izler" #. Translators: help text for qmonitor management command #: management/commands/qmonitor.py:9 msgid "Monitors Q Cluster activity" msgstr "Q Cluster'Äąn aktivitelerini izler" #: models.py:125 msgid "Successful task" msgstr "BaşarÄąlÄą iş" #: models.py:126 msgid "Successful tasks" msgstr "BaşarÄąlÄą işler" #: models.py:141 msgid "Failed task" msgstr "BaşarÄąsÄąz iş" #: models.py:142 msgid "Failed tasks" msgstr "BaşarÄąsÄąz işler" #: models.py:150 models.py:234 msgid "Please install croniter to enable cron expressions" msgstr "Cron expressions'larÄą açmak için croniter yÃŧkleyin" #: models.py:170 msgid "e.g. 1, 2, 'John'" msgstr "Örneğin: 1, 2, 'Melih'" #: models.py:172 msgid "e.g. x=1, y=2, name='John'" msgstr "Örneğin: x=1, y=2, name='Melih'" #: models.py:186 msgid "Once" msgstr "Bir kere" #: models.py:187 msgid "Minutes" msgstr "Dakika" #: models.py:188 msgid "Hourly" msgstr "Saatlik" #: models.py:189 msgid "Daily" msgstr "GÃŧnlÃŧk" #: models.py:190 msgid "Weekly" msgstr "HaftalÄąk" #: models.py:191 #, fuzzy #| msgid "Weekly" msgid "Biweekly" msgstr "İki haftada bir" #: models.py:192 msgid "Monthly" msgstr "AylÄąk" #: models.py:193 #, fuzzy #| msgid "Monthly" msgid "Bimonthly" msgstr "İki ayda bir" #: models.py:194 msgid "Quarterly" msgstr "Bir Çeyrek (3 Ay)" #: models.py:195 msgid "Yearly" msgstr "YÄąllÄąk" #: models.py:196 msgid "Cron" msgstr "" #: models.py:199 msgid "Schedule Type" msgstr "Zamanlama Tipi" #: models.py:202 msgid "Number of minutes for the Minutes type" msgstr "Dakika tipine gÃļre dakika sayÄąsÄą" #: models.py:205 msgid "Repeats" msgstr "Tekrar eder" #: models.py:205 msgid "n = n times, -1 = forever" msgstr "n = n kere, -1 = sonsuza kadar" #: models.py:208 msgid "Next Run" msgstr "Bir dahaki çalÄąÅŸma tarihi" #: models.py:215 msgid "Cron expression" msgstr "" #: models.py:224 msgid "Name of kwarg to pass intended schedule date" msgstr "Geçilecek kwarg'Äąn adÄą ÃļngÃļrÃŧlen program tarihi" #: models.py:299 msgid "Scheduled task" msgstr "ZamanlanmÄąÅŸ iş" #: models.py:300 msgid "Scheduled tasks" msgstr "ZamanlanmÄąÅŸ işler" #: models.py:326 msgid "Queued task" msgstr "SÄąraya alÄąnmÄąÅŸ iş" #: models.py:327 msgid "Queued tasks" msgstr "SÄąraya alÄąnmÄąÅŸ işler" #: monitor.py:64 monitor.py:348 msgid "Host" msgstr "" #: monitor.py:68 monitor.py:352 monitor.py:459 msgid "Id" msgstr "" #: monitor.py:72 msgid "State" msgstr "Durum" #: monitor.py:76 msgid "Pool" msgstr "Havuz" #: monitor.py:80 msgid "TQ" msgstr "" #: monitor.py:84 msgid "RQ" msgstr "" #: monitor.py:88 msgid "RC" msgstr "" #: monitor.py:92 msgid "Up" msgstr "" #: monitor.py:172 monitor.py:286 msgid "Queued" msgstr "SÄąraya alÄąnmÄąÅŸ" #: monitor.py:180 msgid "Success" msgstr "BaşarÄąlÄą olanlar" #: monitor.py:190 monitor.py:294 msgid "Failures" msgstr "BaşarÄąsÄąz olanlar" #: monitor.py:201 monitor.py:498 msgid "[Press q to quit]" msgstr "[Ã‡Äąkmak için q'ya basÄąn]" #: monitor.py:227 msgid "day" msgstr "gÃŧn" #: monitor.py:248 msgid "second" msgstr "saniye" #: monitor.py:251 msgid "minute" msgstr "dakika" #: monitor.py:254 msgid "hour" msgstr "saat" #: monitor.py:263 #, python-format msgid "-- %(prefix)s %(version)s on %(info)s --" msgstr "-- %(prefix)s %(version)s Ãŧzerinde %(info)s --" #: monitor.py:273 msgid "Clusters" msgstr "" #: monitor.py:277 msgid "Workers" msgstr "" #: monitor.py:281 msgid "Restarts" msgstr "Yeniden çalÄąÅŸtÄąrmalar" #: monitor.py:290 msgid "Successes" msgstr "BaşarÄąlÄą olanlar" #: monitor.py:299 msgid "Schedules" msgstr "ZamanlanmÄąÅŸlar" #: monitor.py:303 #, python-format msgid "Tasks/%(per)s" msgstr "İş/%(per)s" #: monitor.py:307 msgid "Avg time" msgstr "Ortalama sÃŧre" #: monitor.py:357 msgid "Available (%)" msgstr "MÃŧsait (%) " #: monitor.py:363 msgid "Available (MB)" msgstr "MÃŧsait (MB)" #: monitor.py:368 msgid "Total (MB)" msgstr "Toplam (MB)" #: monitor.py:373 msgid "Sentinel (MB)" msgstr "" #: monitor.py:379 msgid "Monitor (MB)" msgstr "İzleme (MB)" #: monitor.py:385 msgid "Workers (MB)" msgstr "" #: monitor.py:487 #, python-format msgid "Available lowest (): %(memory_percent)s ((at)s)" msgstr "Mevcut en dÃŧşÃŧk (): %(memory_percent)s ((at)s)" #: monitor.py:509 msgid "No clusters appear to be running." msgstr "Hiçbir kÃŧme çalÄąÅŸÄąyor gÃļrÃŧnmÃŧyor." #: signals.py:22 #, python-format msgid "malformed return hook '%(hook)s' for [%(name)s]" msgstr "" #: signals.py:30 #, python-format msgid "return hook %(hook)s failed on [%(name)s] because %(error)s" msgstr "" #, python-format #~ msgid "" #~ "Could not process '%(func_name)s'. Check the location of the function and " #~ "the args/kwargs." #~ msgstr "" #~ "%(func_name)s' işlenemedi. İşlevin konumunu ve args/kwargs Ãļğelerini " #~ "kontrol edin." django-q2-1.7.4/django_q/management/000077500000000000000000000000001471170400300172005ustar00rootroot00000000000000django-q2-1.7.4/django_q/management/__init__.py000066400000000000000000000000001471170400300212770ustar00rootroot00000000000000django-q2-1.7.4/django_q/management/commands/000077500000000000000000000000001471170400300210015ustar00rootroot00000000000000django-q2-1.7.4/django_q/management/commands/__init__.py000066400000000000000000000000001471170400300231000ustar00rootroot00000000000000django-q2-1.7.4/django_q/management/commands/qcluster.py000066400000000000000000000024001471170400300232110ustar00rootroot00000000000000import os from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from django_q.cluster import Cluster class Command(BaseCommand): # Translators: help text for qcluster management command help = _("Starts a Django Q Cluster.") def add_arguments(self, parser): parser.add_argument( "--run-once", action="store_true", dest="run_once", default=False, help="Run once and then stop.", ) parser.add_argument( "-n", "--name", dest="cluster_name", default=None, help="Set alternative cluster name instead of the name in Q_CLUSTER settings (for multi-queue setup). " "On Linux you should set name through `Q_CLUSTER_NAME=cluster_name python manage.py qcluster` instead.", ) def handle(self, *args, **options): # Set alternative cluster_name before creating the cluster (cluster_name is broker's queue_name, too) cluster_name = options.get("cluster_name") if cluster_name: os.environ["Q_CLUSTER_NAME"] = cluster_name q = Cluster() q.start() if options.get("run_once", False): q.stop() django-q2-1.7.4/django_q/management/commands/qinfo.py000066400000000000000000000030471471170400300224730ustar00rootroot00000000000000from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from django_q import VERSION from django_q.conf import Conf from django_q.monitor_terminal import get_ids, info class Command(BaseCommand): # Translators: help text for qinfo management command help = _("General information over all clusters.") def add_arguments(self, parser): parser.add_argument( "--config", action="store_true", dest="config", default=False, help="Print current configuration.", ) parser.add_argument( "--ids", action="store_true", dest="ids", default=False, help="Print cluster task ID(s) (PIDs).", ) def handle(self, *args, **options): if options.get("ids", True): get_ids() elif options.get("config", False): hide = [ "conf", "IDLE", "STOPPING", "STARTING", "WORKING", "SIGNAL_NAMES", "STOPPED", ] settings = [ a for a in dir(Conf) if not a.startswith("__") and a not in hide ] self.stdout.write(f"VERSION: {'.'.join(str(v) for v in VERSION)}") for setting in settings: value = getattr(Conf, setting) if value is not None: self.stdout.write(f"{setting}: {value}") else: info() django-q2-1.7.4/django_q/management/commands/qmemory.py000066400000000000000000000016021471170400300230430ustar00rootroot00000000000000from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from django_q.monitor_terminal import memory class Command(BaseCommand): # Translators: help text for qmemory management command help = _("Monitors Q Cluster memory usage") def add_arguments(self, parser): parser.add_argument( "--run-once", action="store_true", dest="run_once", default=False, help="Run once and then stop.", ) parser.add_argument( "--workers", action="store_true", dest="workers", default=False, help="Show each worker's memory usage.", ) def handle(self, *args, **options): memory( run_once=options.get("run_once", False), workers=options.get("workers", False), ) django-q2-1.7.4/django_q/management/commands/qmonitor.py000066400000000000000000000011521471170400300232220ustar00rootroot00000000000000from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from django_q.monitor_terminal import monitor class Command(BaseCommand): # Translators: help text for qmonitor management command help = _("Monitors Q Cluster activity") def add_arguments(self, parser): parser.add_argument( "--run-once", action="store_true", dest="run_once", default=False, help="Run once and then stop.", ) def handle(self, *args, **options): monitor(run_once=options.get("run_once", False)) django-q2-1.7.4/django_q/migrations/000077500000000000000000000000001471170400300172405ustar00rootroot00000000000000django-q2-1.7.4/django_q/migrations/0001_initial.py000066400000000000000000000114251471170400300217060ustar00rootroot00000000000000import django.utils.timezone import picklefield.fields from django.db import migrations, models class Migration(migrations.Migration): dependencies = [] operations = [ migrations.CreateModel( name="Schedule", fields=[ ( "id", models.AutoField( verbose_name="ID", auto_created=True, serialize=False, primary_key=True, ), ), ( "func", models.CharField( max_length=256, help_text="e.g. module.tasks.function" ), ), ( "hook", models.CharField( null=True, blank=True, max_length=256, help_text="e.g. module.tasks.result_function", ), ), ( "args", models.CharField( null=True, blank=True, max_length=256, help_text="e.g. 1, 2, 'John'", ), ), ( "kwargs", models.CharField( null=True, blank=True, max_length=256, help_text="e.g. x=1, y=2, name='John'", ), ), ( "schedule_type", models.CharField( verbose_name="Schedule Type", choices=[ ("O", "Once"), ("H", "Hourly"), ("D", "Daily"), ("W", "Weekly"), ("M", "Monthly"), ("Q", "Quarterly"), ("Y", "Yearly"), ], default="O", max_length=1, ), ), ( "repeats", models.SmallIntegerField( verbose_name="Repeats", default=-1, help_text="n = n times, -1 = forever", ), ), ( "next_run", models.DateTimeField( verbose_name="Next Run", default=django.utils.timezone.now, null=True, ), ), ("task", models.CharField(editable=False, null=True, max_length=100)), ], options={ "verbose_name": "Scheduled task", "ordering": ["next_run"], }, ), migrations.CreateModel( name="Task", fields=[ ( "id", models.AutoField( verbose_name="ID", auto_created=True, serialize=False, primary_key=True, ), ), ("name", models.CharField(editable=False, max_length=100)), ("func", models.CharField(max_length=256)), ("hook", models.CharField(null=True, max_length=256)), ( "args", picklefield.fields.PickledObjectField(editable=False, null=True), ), ( "kwargs", picklefield.fields.PickledObjectField(editable=False, null=True), ), ( "result", picklefield.fields.PickledObjectField(editable=False, null=True), ), ("started", models.DateTimeField(editable=False)), ("stopped", models.DateTimeField(editable=False)), ("success", models.BooleanField(editable=False, default=True)), ], ), migrations.CreateModel( name="Failure", fields=[], options={ "verbose_name": "Failed task", "proxy": True, }, bases=("django_q.task",), ), migrations.CreateModel( name="Success", fields=[], options={ "verbose_name": "Successful task", "proxy": True, }, bases=("django_q.task",), ), ] django-q2-1.7.4/django_q/migrations/0002_auto_20150630_1624.py000066400000000000000000000011641471170400300226610ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0001_initial"), ] operations = [ migrations.AlterField( model_name="schedule", name="args", field=models.TextField( help_text="e.g. 1, 2, 'John'", blank=True, null=True ), ), migrations.AlterField( model_name="schedule", name="kwargs", field=models.TextField( help_text="e.g. x=1, y=2, name='John'", blank=True, null=True ), ), ] django-q2-1.7.4/django_q/migrations/0003_auto_20150708_1326.py000066400000000000000000000022741471170400300226720ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0002_auto_20150630_1624"), ] operations = [ migrations.AlterModelOptions( name="failure", options={ "verbose_name_plural": "Failed tasks", "verbose_name": "Failed task", }, ), migrations.AlterModelOptions( name="schedule", options={ "verbose_name_plural": "Scheduled tasks", "ordering": ["next_run"], "verbose_name": "Scheduled task", }, ), migrations.AlterModelOptions( name="success", options={ "verbose_name_plural": "Successful tasks", "verbose_name": "Successful task", }, ), migrations.RemoveField( model_name="task", name="id", ), migrations.AddField( model_name="task", name="id", field=models.CharField( max_length=32, primary_key=True, editable=False, serialize=False ), ), ] django-q2-1.7.4/django_q/migrations/0004_auto_20150710_1043.py000066400000000000000000000014661471170400300226620ustar00rootroot00000000000000from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("django_q", "0003_auto_20150708_1326"), ] operations = [ migrations.AlterModelOptions( name="failure", options={ "verbose_name_plural": "Failed tasks", "verbose_name": "Failed task", "ordering": ["-stopped"], }, ), migrations.AlterModelOptions( name="success", options={ "verbose_name_plural": "Successful tasks", "verbose_name": "Successful task", "ordering": ["-stopped"], }, ), migrations.AlterModelOptions( name="task", options={"ordering": ["-stopped"]}, ), ] django-q2-1.7.4/django_q/migrations/0005_auto_20150718_1506.py000066400000000000000000000010131471170400300226630ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0004_auto_20150710_1043"), ] operations = [ migrations.AddField( model_name="schedule", name="name", field=models.CharField(max_length=100, null=True), ), migrations.AddField( model_name="task", name="group", field=models.CharField(max_length=100, null=True, editable=False), ), ] django-q2-1.7.4/django_q/migrations/0006_auto_20150805_1817.py000066400000000000000000000020771471170400300227010ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0005_auto_20150718_1506"), ] operations = [ migrations.AddField( model_name="schedule", name="minutes", field=models.PositiveSmallIntegerField( help_text="Number of minutes for the Minutes type", blank=True, null=True, ), ), migrations.AlterField( model_name="schedule", name="schedule_type", field=models.CharField( max_length=1, choices=[ ("O", "Once"), ("I", "Minutes"), ("H", "Hourly"), ("D", "Daily"), ("W", "Weekly"), ("M", "Monthly"), ("Q", "Quarterly"), ("Y", "Yearly"), ], default="O", verbose_name="Schedule Type", ), ), ] django-q2-1.7.4/django_q/migrations/0007_ormq.py000066400000000000000000000015671471170400300212470ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0006_auto_20150805_1817"), ] operations = [ migrations.CreateModel( name="OrmQ", fields=[ ( "id", models.AutoField( primary_key=True, auto_created=True, verbose_name="ID", serialize=False, ), ), ("key", models.CharField(max_length=100)), ("payload", models.TextField()), ("lock", models.DateTimeField(null=True)), ], options={ "verbose_name_plural": "Queued tasks", "verbose_name": "Queued task", }, ), ] django-q2-1.7.4/django_q/migrations/0008_auto_20160224_1026.py000066400000000000000000000005331471170400300226620ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0007_ormq"), ] operations = [ migrations.AlterField( model_name="schedule", name="name", field=models.CharField(blank=True, max_length=100, null=True), ), ] django-q2-1.7.4/django_q/migrations/0009_auto_20171009_0915.py000066400000000000000000000007221471170400300226740ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0008_auto_20160224_1026"), ] operations = [ migrations.AlterField( model_name="schedule", name="repeats", field=models.IntegerField( default=-1, help_text="n = n times, -1 = forever", verbose_name="Repeats", ), ), ] django-q2-1.7.4/django_q/migrations/0010_auto_20200610_0856.py000066400000000000000000000015611471170400300226610ustar00rootroot00000000000000import picklefield.fields from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("django_q", "0009_auto_20171009_0915"), ] operations = [ migrations.AlterField( model_name="task", name="args", field=picklefield.fields.PickledObjectField( editable=False, null=True, protocol=-1 ), ), migrations.AlterField( model_name="task", name="kwargs", field=picklefield.fields.PickledObjectField( editable=False, null=True, protocol=-1 ), ), migrations.AlterField( model_name="task", name="result", field=picklefield.fields.PickledObjectField( editable=False, null=True, protocol=-1 ), ), ] django-q2-1.7.4/django_q/migrations/0011_auto_20200628_1055.py000066400000000000000000000021301471170400300226540ustar00rootroot00000000000000# Generated by Django 3.0.7 on 2020-06-28 10:55 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0010_auto_20200610_0856"), ] operations = [ migrations.AddField( model_name="schedule", name="cron", field=models.CharField( blank=True, help_text="Cron expression", max_length=100, null=True ), ), migrations.AlterField( model_name="schedule", name="schedule_type", field=models.CharField( choices=[ ("O", "Once"), ("I", "Minutes"), ("H", "Hourly"), ("D", "Daily"), ("W", "Weekly"), ("M", "Monthly"), ("Q", "Quarterly"), ("Y", "Yearly"), ("C", "Cron"), ], default="O", max_length=1, verbose_name="Schedule Type", ), ), ] django-q2-1.7.4/django_q/migrations/0012_auto_20200702_1608.py000066400000000000000000000011321471170400300226530ustar00rootroot00000000000000# Generated by Django 3.0.8 on 2020-07-02 16:08 from django.db import migrations, models import django_q.models class Migration(migrations.Migration): dependencies = [ ("django_q", "0011_auto_20200628_1055"), ] operations = [ migrations.AlterField( model_name="schedule", name="cron", field=models.CharField( blank=True, help_text="Cron expression", max_length=100, null=True, validators=[django_q.models.validate_cron], ), ), ] django-q2-1.7.4/django_q/migrations/0013_task_attempt_count.py000066400000000000000000000006041471170400300241650ustar00rootroot00000000000000# Generated by Django 3.0.7 on 2020-08-11 15:17 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0012_auto_20200702_1608"), ] operations = [ migrations.AddField( model_name="task", name="attempt_count", field=models.IntegerField(default=0), ), ] django-q2-1.7.4/django_q/migrations/0014_schedule_cluster.py000066400000000000000000000006511471170400300236150ustar00rootroot00000000000000# Generated by Django 3.2.2 on 2021-05-11 05:59 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0013_task_attempt_count"), ] operations = [ migrations.AddField( model_name="schedule", name="cluster", field=models.CharField(blank=True, default=None, max_length=100, null=True), ), ] django-q2-1.7.4/django_q/migrations/0015_alter_schedule_schedule_type.py000066400000000000000000000016751471170400300261700ustar00rootroot00000000000000# Generated by Django 4.1.2 on 2022-11-10 01:35 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0014_schedule_cluster"), ] operations = [ migrations.AlterField( model_name="schedule", name="schedule_type", field=models.CharField( choices=[ ("O", "Once"), ("I", "Minutes"), ("H", "Hourly"), ("D", "Daily"), ("W", "Weekly"), ("BW", "Biweekly"), ("M", "Monthly"), ("BM", "Bimonthly"), ("Q", "Quarterly"), ("Y", "Yearly"), ("C", "Cron"), ], default="O", max_length=2, verbose_name="Schedule Type", ), ), ] django-q2-1.7.4/django_q/migrations/0016_schedule_intended_date_kwarg.py000066400000000000000000000012171471170400300261170ustar00rootroot00000000000000# Generated by Django 4.1.2 on 2023-01-15 22:34 from django.db import migrations, models import django_q.models class Migration(migrations.Migration): dependencies = [ ("django_q", "0015_alter_schedule_schedule_type"), ] operations = [ migrations.AddField( model_name="schedule", name="intended_date_kwarg", field=models.CharField( blank=True, help_text="Name of kwarg to pass intended schedule date", max_length=100, null=True, validators=[django_q.models.validate_kwarg], ), ), ] django-q2-1.7.4/django_q/migrations/0017_task_cluster_alter.py000066400000000000000000000022721471170400300241560ustar00rootroot00000000000000# Generated by Django 4.1.5 on 2023-03-07 12:18 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0016_schedule_intended_date_kwarg"), ] operations = [ migrations.AddField( model_name="task", name="cluster", field=models.CharField(blank=True, default=None, max_length=100, null=True), ), migrations.AlterField( model_name="ormq", name="key", field=models.CharField( help_text="Name of the target cluster", max_length=100 ), ), migrations.AlterField( model_name="ormq", name="lock", field=models.DateTimeField( help_text="Prevent any cluster from pulling until", null=True ), ), migrations.AlterField( model_name="schedule", name="cluster", field=models.CharField( blank=True, default=None, help_text="Name of the target cluster", max_length=100, null=True, ), ), ] django-q2-1.7.4/django_q/migrations/0018_task_success_index.py000066400000000000000000000007561471170400300241530ustar00rootroot00000000000000# Generated by Django 4.2.7 on 2024-03-05 17:03 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("django_q", "0017_task_cluster_alter"), ] operations = [ migrations.AddIndex( model_name="task", index=models.Index( condition=models.Q(("success", True)), fields=["group", "name", "func"], name="success_index", ), ), ] django-q2-1.7.4/django_q/migrations/__init__.py000066400000000000000000000000001471170400300213370ustar00rootroot00000000000000django-q2-1.7.4/django_q/models.py000066400000000000000000000272531471170400300167320ustar00rootroot00000000000000from datetime import datetime, timedelta from keyword import iskeyword # Django from django import get_version from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from django.template.defaultfilters import truncatechars from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ # External from picklefield import PickledObjectField from picklefield.fields import dbsafe_decode # Local from django_q.conf import croniter from django_q.signing import SignedPackage from django_q.utils import add_months, add_years, localtime from .utils import get_func_repr class Task(models.Model): id = models.CharField(max_length=32, primary_key=True, editable=False) name = models.CharField(max_length=100, editable=False) func = models.CharField(max_length=256) hook = models.CharField(max_length=256, null=True) args = PickledObjectField(null=True, protocol=-1) kwargs = PickledObjectField(null=True, protocol=-1) result = PickledObjectField(null=True, protocol=-1) group = models.CharField(max_length=100, editable=False, null=True) cluster = models.CharField(max_length=100, default=None, null=True, blank=True) started = models.DateTimeField(editable=False) stopped = models.DateTimeField(editable=False) success = models.BooleanField(default=True, editable=False) attempt_count = models.IntegerField(default=0) @staticmethod def get_result(task_id): if len(task_id) == 32 and Task.objects.filter(id=task_id).exists(): return Task.objects.get(id=task_id).result elif Task.objects.filter(name=task_id).exists(): return Task.objects.get(name=task_id).result @staticmethod def get_result_group(group_id, failures=False): if failures: values = Task.objects.filter(group=group_id).values_list( "result", flat=True ) else: values = ( Task.objects.filter(group=group_id) .exclude(success=False) .values_list("result", flat=True) ) return decode_results(values) def group_result(self, failures=False): if self.group: return self.get_result_group(self.group, failures) @staticmethod def get_group_count(group_id, failures=False): if failures: return Failure.objects.filter(group=group_id).count() return Task.objects.filter(group=group_id).count() def group_count(self, failures=False): if self.group: return self.get_group_count(self.group, failures) @staticmethod def delete_group(group_id, objects=False): group = Task.objects.filter(group=group_id) if objects: return group.delete() return group.update(group=None) def group_delete(self, tasks=False): if self.group: return self.delete_group(self.group, tasks) @staticmethod def get_task(task_id): if len(task_id) == 32 and Task.objects.filter(id=task_id).exists(): return Task.objects.get(id=task_id) elif Task.objects.filter(name=task_id).exists(): return Task.objects.get(name=task_id) @staticmethod def get_task_group(group_id, failures=True): if failures: return Task.objects.filter(group=group_id) return Task.objects.filter(group=group_id).exclude(success=False) def time_taken(self): return (self.stopped - self.started).total_seconds() @property def short_result(self): return truncatechars(self.result, 100) def __str__(self): return f"{self.name or self.id}" class Meta: app_label = "django_q" ordering = ["-stopped"] indexes = [ models.Index( name="success_index", fields=["group", "name", "func"], condition=Q(success=True), ), ] class SuccessManager(models.Manager): def get_queryset(self): return super(SuccessManager, self).get_queryset().filter(success=True) class Success(Task): objects = SuccessManager() class Meta: app_label = "django_q" verbose_name = _("Successful task") verbose_name_plural = _("Successful tasks") ordering = ["-stopped"] proxy = True class FailureManager(models.Manager): def get_queryset(self): return super(FailureManager, self).get_queryset().filter(success=False) class Failure(Task): objects = FailureManager() class Meta: app_label = "django_q" verbose_name = _("Failed task") verbose_name_plural = _("Failed tasks") ordering = ["-stopped"] proxy = True # Optional Cron validator def validate_cron(value): if not croniter: raise ImportError(_("Please install croniter to enable cron expressions")) try: croniter.expand(value) except ValueError as e: raise ValidationError(e) def validate_kwarg(value): return value.isidentifier() and not iskeyword(value) class Schedule(models.Model): name = models.CharField(max_length=100, null=True, blank=True) func = models.CharField(max_length=256, help_text="e.g. module.tasks.function") hook = models.CharField( max_length=256, null=True, blank=True, help_text="e.g. module.tasks.result_function", ) args = models.TextField(null=True, blank=True, help_text=_("e.g. 1, 2, 'John'")) kwargs = models.TextField( null=True, blank=True, help_text=_("e.g. x=1, y=2, name='John'") ) ONCE = "O" MINUTES = "I" HOURLY = "H" DAILY = "D" WEEKLY = "W" BIWEEKLY = "BW" MONTHLY = "M" BIMONTHLY = "BM" QUARTERLY = "Q" YEARLY = "Y" CRON = "C" TYPE = ( (ONCE, _("Once")), (MINUTES, _("Minutes")), (HOURLY, _("Hourly")), (DAILY, _("Daily")), (WEEKLY, _("Weekly")), (BIWEEKLY, _("Biweekly")), (MONTHLY, _("Monthly")), (BIMONTHLY, _("Bimonthly")), (QUARTERLY, _("Quarterly")), (YEARLY, _("Yearly")), (CRON, _("Cron")), ) schedule_type = models.CharField( max_length=2, choices=TYPE, default=TYPE[0][0], verbose_name=_("Schedule Type") ) minutes = models.PositiveSmallIntegerField( null=True, blank=True, help_text=_("Number of minutes for the Minutes type") ) repeats = models.IntegerField( default=-1, verbose_name=_("Repeats"), help_text=_("n = n times, -1 = forever") ) next_run = models.DateTimeField( verbose_name=_("Next Run"), default=timezone.now, null=True ) cron = models.CharField( max_length=100, null=True, blank=True, validators=[validate_cron], help_text=_("Cron expression"), ) task = models.CharField(max_length=100, null=True, editable=False) cluster = models.CharField( max_length=100, default=None, null=True, blank=True, help_text=_("Name of the target cluster"), ) intended_date_kwarg = models.CharField( max_length=100, null=True, blank=True, validators=[validate_kwarg], help_text=_("Name of kwarg to pass intended schedule date"), ) def calculate_next_run(self, next_run=None): # next run is always in UTC next_run = next_run or self.next_run if self.schedule_type == self.CRON: if not croniter: raise ImportError( _("Please install croniter to enable cron expressions") ) return croniter(self.cron, localtime()).get_next(datetime) if self.schedule_type == self.MINUTES: add = timedelta(minutes=(self.minutes or 1)) elif self.schedule_type == self.HOURLY: add = timedelta(hours=1) elif self.schedule_type == self.DAILY: add = timedelta(days=1) elif self.schedule_type == self.WEEKLY: add = timedelta(weeks=1) elif self.schedule_type == self.BIWEEKLY: add = timedelta(weeks=2) elif self.schedule_type == self.MONTHLY: add = timedelta(days=(add_months(next_run, 1) - next_run).days) elif self.schedule_type == self.BIMONTHLY: add = timedelta(days=(add_months(next_run, 2) - next_run).days) elif self.schedule_type == self.QUARTERLY: add = timedelta(days=(add_months(next_run, 3) - next_run).days) elif self.schedule_type == self.YEARLY: add = timedelta(days=(add_years(next_run, 1) - next_run).days) # add normal timedelta, we will correct this later based on timezone next_run += add # DST differencers don't matter with minutes, hourly or yearly, so skip those if self.schedule_type not in [self.MINUTES, self.HOURLY, self.YEARLY]: # Get localtimes and then remove the tzinfo, so we can get the actual difference current_next_run = localtime(next_run - add).replace(tzinfo=None) new_next_run = localtime(next_run).replace(tzinfo=None) # get the difference between them, this should be (-)1 or (-)0.5 hour # based on DST active or not extra_diff = (new_next_run - current_next_run) - add # subtract difference next_run -= extra_diff return next_run def success(self): if self.task and Task.objects.filter(id=self.task): return Task.objects.get(id=self.task).success def last_run(self): if self.task and Task.objects.filter(id=self.task): task = Task.objects.get(id=self.task) if task.success: url = reverse("admin:django_q_success_change", args=(task.id,)) else: url = reverse("admin:django_q_failure_change", args=(task.id,)) return format_html(f'[{task.name}]') return None def __str__(self): return self.func success.boolean = True success.short_description = _("success") last_run.allow_tags = True last_run.short_description = _("last_run") class Meta: app_label = "django_q" verbose_name = _("Scheduled task") verbose_name_plural = _("Scheduled tasks") ordering = ["next_run"] class OrmQ(models.Model): key = models.CharField(max_length=100, help_text=_("Name of the target cluster")) payload = models.TextField() lock = models.DateTimeField( null=True, help_text=_("Prevent any cluster from pulling until") ) @cached_property def task(self): try: return SignedPackage.loads(self.payload) except Exception as e: return {"id": "*" + e.__class__.__name__} def func(self): return get_func_repr(self.task.get("func")) def task_id(self): return self.task.get("id") def name(self): return self.task.get("name") def group(self): return self.task.get("group") def args(self): return self.task.get("args") def kwargs(self): return self.task.get("kwargs") def q_options(self): exclude = {"id", "name", "group", "func", "args", "kwargs"} return {k: v for k, v in self.task.items() if k not in exclude} class Meta: app_label = "django_q" verbose_name = _("Queued task") verbose_name_plural = _("Queued tasks") # Backwards compatibility for Django 1.7 def decode_results(values): if get_version().split(".")[1] == "7": # decode values in 1.7 return [dbsafe_decode(v) for v in values] return values django-q2-1.7.4/django_q/monitor.py000066400000000000000000000164041471170400300171320ustar00rootroot00000000000000from multiprocessing.process import current_process from multiprocessing.queues import Queue from django import core, db from django.apps.registry import apps from django.utils.translation import gettext_lazy as _ try: apps.check_apps_ready() except core.exceptions.AppRegistryNotReady: import django django.setup() from django_q.brokers import Broker, get_broker from django_q.conf import Conf, logger, setproctitle from django_q.models import Success, Task from django_q.signals import post_execute from django_q.signing import SignedPackage from django_q.tasks import async_chain from django_q.utils import close_old_django_connections, get_func_repr try: import setproctitle except ModuleNotFoundError: setproctitle = None def monitor(result_queue: Queue, broker: Broker = None): """ Gets finished tasks from the result queue and saves them to Django :type broker: brokers.Broker :type result_queue: multiprocessing.Queue """ if not broker: broker = get_broker() proc_name = current_process().name if setproctitle: setproctitle.setproctitle(f"qcluster {proc_name} monitor") logger.info( _("%(name)s monitoring at %(id)s") % {"name": proc_name, "id": current_process().pid} ) for task in iter(result_queue.get, "STOP"): # save the result if task.get("cached", False): save_cached(task, broker) else: save_task(task, broker) # acknowledge result ack_id = task.pop("ack_id", False) if ack_id and (task["success"] or task.get("ack_failure", False)): broker.acknowledge(ack_id) # signal execution done post_execute.send(sender="django_q", task=task) # log the result info_name = get_func_repr(task["func"]) if task["success"]: # log success logger.info( _("Processed '%(info_name)s' (%(task_name)s)") % {"info_name": info_name, "task_name": task["name"]} ) else: # log failure logger.error( _("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s") % { "info_name": info_name, "task_name": task["name"], "task_result": task["result"], } ) logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name}) def save_task(task, broker: Broker): """ Saves the task package to Django or the cache :param task: the task package :type broker: brokers.Broker """ # SAVE LIMIT < 0 : Don't save success if not task.get("save", Conf.SAVE_LIMIT >= 0) and task["success"]: return # enqueues next in a chain if task.get("chain", None): async_chain( task["chain"], group=task["group"], cached=task["cached"], sync=task["sync"], broker=broker, ) # SAVE LIMIT > 0: Prune database, SAVE_LIMIT 0: No pruning close_old_django_connections() try: filters = {} if ( Conf.SAVE_LIMIT_PER and Conf.SAVE_LIMIT_PER in {"group", "name", "func"} and Conf.SAVE_LIMIT_PER in task ): value = task[Conf.SAVE_LIMIT_PER] if Conf.SAVE_LIMIT_PER == "func": value = get_func_repr(value) filters[Conf.SAVE_LIMIT_PER] = value with db.transaction.atomic(using=db.router.db_for_write(Success)): list(Success.objects.filter(**filters).select_for_update()) if ( task["success"] and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count() ): Success.objects.filter(**filters).last().delete() # check if this task has previous results try: task_obj = Task.objects.get(id=task["id"], name=task["name"]) # only update the result if it hasn't succeeded yet if not task_obj.success: task_obj.stopped = task["stopped"] task_obj.result = task["result"] task_obj.success = task["success"] task_obj.attempt_count = task_obj.attempt_count + 1 task_obj.save() except Task.DoesNotExist: # convert func to string func = get_func_repr(task["func"]) task_obj = Task.objects.create( id=task["id"], name=task["name"], func=func, hook=task.get("hook"), args=task["args"], kwargs=task["kwargs"], cluster=task.get("cluster"), started=task["started"], stopped=task["stopped"], result=task["result"], group=task.get("group"), success=task["success"], attempt_count=1, ) if ( Conf.MAX_ATTEMPTS > 0 and task_obj.attempt_count >= Conf.MAX_ATTEMPTS and task.get("ack_id") ): broker.acknowledge(task["ack_id"]) except Exception: logger.exception("Could not save task result") def save_cached(task, broker: Broker): task_key = f'{broker.list_key}:{task["id"]}' timeout = task["cached"] if timeout is True: timeout = None try: group = task.get("group", None) iter_count = task.get("iter_count", 0) # if it's a group append to the group list if group: group_key = f"{broker.list_key}:{group}:keys" group_list = broker.cache.get(group_key) or [] # if it's an iter group, check if we are ready if iter_count and len(group_list) == iter_count - 1: group_args = f"{broker.list_key}:{group}:args" # collate the results into a Task result results = [ SignedPackage.loads(broker.cache.get(k))["result"] for k in group_list ] results.append(task["result"]) task["result"] = results task["id"] = group task["args"] = SignedPackage.loads(broker.cache.get(group_args)) task.pop("iter_count", None) task.pop("group", None) if task.get("iter_cached", None): task["cached"] = task.pop("iter_cached", None) save_cached(task, broker=broker) else: save_task(task, broker) broker.cache.delete_many(group_list) broker.cache.delete_many([group_key, group_args]) return # save the group list group_list.append(task_key) broker.cache.set(group_key, group_list, timeout) # async_task next in a chain if task.get("chain", None): async_chain( task["chain"], group=group, cached=task["cached"], sync=task["sync"], broker=broker, ) # save the task broker.cache.set(task_key, SignedPackage.dumps(task), timeout) except Exception: logger.exception("Could not save task result") django-q2-1.7.4/django_q/monitor_terminal.py000066400000000000000000000434311471170400300210250ustar00rootroot00000000000000from datetime import timedelta # django from django.db import connection from django.db.models import F, Sum from django.utils import timezone from django.utils.translation import gettext as _ from django_q import VERSION, models from django_q.brokers import get_broker # local from django_q.conf import Conf from django_q.status import Stat # optional try: import psutil except ImportError: psutil = None def get_process_mb(pid): try: process = psutil.Process(pid) mb_used = round(process.memory_info().rss / 1024**2, 2) except psutil.NoSuchProcess: mb_used = "NO_PROCESS_FOUND" return mb_used BLESSED_INSTALL_MESSAGE = ( "Blessed is not installed. Please install blessed to use this: " "https://pypi.org/project/blessed/" ) def monitor(run_once=False, broker=None): if not broker: broker = get_broker() try: from blessed import Terminal term = Terminal() except ImportError: print(BLESSED_INSTALL_MESSAGE) return broker.ping() with term.fullscreen(), term.hidden_cursor(), term.cbreak(): val = None start_width = int(term.width / 8) while val not in ( "q", "Q", ): col_width = int(term.width / 8) # In case of resize if col_width != start_width: print(term.clear()) start_width = col_width print( term.move(0, 0) + term.black_on_green(term.center(_("Host"), width=col_width - 1)) ) print( term.move(0, 1 * col_width) + term.black_on_green(term.center(_("Id"), width=col_width - 1)) ) print( term.move(0, 2 * col_width) + term.black_on_green(term.center(_("State"), width=col_width - 1)) ) print( term.move(0, 3 * col_width) + term.black_on_green(term.center(_("Pool"), width=col_width - 1)) ) print( term.move(0, 4 * col_width) + term.black_on_green(term.center(_("TQ"), width=col_width - 1)) ) print( term.move(0, 5 * col_width) + term.black_on_green(term.center(_("RQ"), width=col_width - 1)) ) print( term.move(0, 6 * col_width) + term.black_on_green(term.center(_("RC"), width=col_width - 1)) ) print( term.move(0, 7 * col_width) + term.black_on_green(term.center(_("Up"), width=col_width - 1)) ) i = 2 stats = Stat.get_all(broker=broker) print(term.clear_eos()) for stat in stats: status = stat.status # color status if stat.status == Conf.WORKING: status = term.green(str(Conf.WORKING)) elif stat.status == Conf.STOPPING: status = term.yellow(str(Conf.STOPPING)) elif stat.status == Conf.STOPPED: status = term.red(str(Conf.STOPPED)) elif stat.status == Conf.IDLE: status = str(Conf.IDLE) # color q's tasks = str(stat.task_q_size) if stat.task_q_size > 0: tasks = term.cyan(str(stat.task_q_size)) if Conf.QUEUE_LIMIT and stat.task_q_size == Conf.QUEUE_LIMIT: tasks = term.green(str(stat.task_q_size)) results = stat.done_q_size if results > 0: results = term.cyan(str(results)) # color workers workers = len(stat.workers) if workers < Conf.WORKERS: workers = term.yellow(str(workers)) # format uptime uptime = (timezone.now() - stat.tob).total_seconds() hours, remainder = divmod(uptime, 3600) minutes, seconds = divmod(remainder, 60) uptime = "%d:%02d:%02d" % (hours, minutes, seconds) # print to the terminal print( term.move(i, 0) + term.center(stat.host[: col_width - 1], width=col_width - 1) ) print( term.move(i, 1 * col_width) + term.center(str(stat.cluster_id)[-8:], width=col_width - 1) ) print( term.move(i, 2 * col_width) + term.center(status, width=col_width - 1) ) print( term.move(i, 3 * col_width) + term.center(workers, width=col_width - 1) ) print( term.move(i, 4 * col_width) + term.center(tasks, width=col_width - 1) ) print( term.move(i, 5 * col_width) + term.center(results, width=col_width - 1) ) print( term.move(i, 6 * col_width) + term.center(stat.reincarnations, width=col_width - 1) ) print( term.move(i, 7 * col_width) + term.center(uptime, width=col_width - 1) ) i += 1 # bottom bar i += 1 queue_size = broker.queue_size() lock_size = broker.lock_size() if lock_size: queue_size = f"{queue_size}({lock_size})" print( term.move(i, 0) + term.white_on_cyan(term.center(broker.info(), width=col_width * 2)) ) print( term.move(i, 2 * col_width) + term.black_on_cyan(term.center(_("Queued"), width=col_width)) ) print( term.move(i, 3 * col_width) + term.white_on_cyan(term.center(queue_size, width=col_width)) ) print( term.move(i, 4 * col_width) + term.black_on_cyan(term.center(_("Success"), width=col_width)) ) print( term.move(i, 5 * col_width) + term.white_on_cyan( term.center(models.Success.objects.count(), width=col_width) ) ) print( term.move(i, 6 * col_width) + term.black_on_cyan(term.center(_("Failures"), width=col_width)) ) print( term.move(i, 7 * col_width) + term.white_on_cyan( term.center(models.Failure.objects.count(), width=col_width) ) ) # for testing if run_once: return Stat.get_all(broker=broker) print(term.move(i + 2, 0) + term.center(_("[Press q to quit]"))) val = term.inkey(timeout=1) def info(broker=None): if not broker: broker = get_broker() try: from blessed import Terminal term = Terminal() except ImportError: print(BLESSED_INSTALL_MESSAGE) return broker.ping() stat = Stat.get_all(broker=broker) # general stats clusters = len(stat) workers = 0 reincarnations = 0 for cluster in stat: workers += len(cluster.workers) reincarnations += cluster.reincarnations # calculate tasks pm and avg exec time tasks_per = 0 per = _("day") exec_time = 0 last_tasks = models.Success.objects.filter( stopped__gte=timezone.now() - timedelta(hours=24) ) tasks_per_day = last_tasks.count() if tasks_per_day > 0: # average execution time over the last 24 hours if connection.vendor != "sqlite": exec_time = last_tasks.aggregate( time_taken=Sum(F("stopped") - F("started")) ) exec_time = exec_time["time_taken"].total_seconds() / tasks_per_day else: # can't sum timedeltas on sqlite for t in last_tasks: exec_time += t.time_taken() exec_time = exec_time / tasks_per_day # tasks per second/minute/hour/day in the last 24 hours if tasks_per_day > 24 * 60 * 60: tasks_per = tasks_per_day / (24 * 60 * 60) per = _("second") elif tasks_per_day > 24 * 60: tasks_per = tasks_per_day / (24 * 60) per = _("minute") elif tasks_per_day > 24: tasks_per = tasks_per_day / 24 per = _("hour") else: tasks_per = tasks_per_day # print to terminal print(term.clear_eos()) col_width = int(term.width / 6) print( term.black_on_green( term.center( _("-- %(prefix)s %(version)s on %(info)s --") % { "prefix": Conf.PREFIX.capitalize(), "version": ".".join(str(v) for v in VERSION), "info": broker.info(), } ) ) ) print( term.cyan(_("Clusters")) + term.move_x(1 * col_width) + term.white(str(clusters)) + term.move_x(2 * col_width) + term.cyan(_("Workers")) + term.move_x(3 * col_width) + term.white(str(workers)) + term.move_x(4 * col_width) + term.cyan(_("Restarts")) + term.move_x(5 * col_width) + term.white(str(reincarnations)) ) print( term.cyan(_("Queued")) + term.move_x(1 * col_width) + term.white(str(broker.queue_size())) + term.move_x(2 * col_width) + term.cyan(_("Successes")) + term.move_x(3 * col_width) + term.white(str(models.Success.objects.count())) + term.move_x(4 * col_width) + term.cyan(_("Failures")) + term.move_x(5 * col_width) + term.white(str(models.Failure.objects.count())) ) print( term.cyan(_("Schedules")) + term.move_x(1 * col_width) + term.white(str(models.Schedule.objects.count())) + term.move_x(2 * col_width) + term.cyan(_("Tasks/%(per)s") % {"per": per}) + term.move_x(3 * col_width) + term.white(f"{tasks_per:.2f}") + term.move_x(4 * col_width) + term.cyan(_("Avg time")) + term.move_x(5 * col_width) + term.white(f"{exec_time:.4f}") ) return True def memory(run_once=False, workers=False, broker=None): if not broker: broker = get_broker() try: from blessed import Terminal term = Terminal() except ImportError: print(BLESSED_INSTALL_MESSAGE) return broker.ping() if not psutil: print(term.clear_eos()) print( term.white_on_red( 'Cannot start "qmemory" command. Missing "psutil" library.' ) ) return with term.fullscreen(), term.hidden_cursor(), term.cbreak(): MEMORY_AVAILABLE_LOWEST_PERCENTAGE = 100.0 MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now() cols = 8 val = None start_width = int(term.width / cols) while val not in ["q", "Q"]: col_width = int(term.width / cols) # In case of resize if col_width != start_width: print(term.clear()) start_width = col_width # sentinel, monitor and workers memory usage print( term.move(0, 0 * col_width) + term.black_on_green(term.center(_("Host"), width=col_width - 1)) ) print( term.move(0, 1 * col_width) + term.black_on_green(term.center(_("Id"), width=col_width - 1)) ) print( term.move(0, 2 * col_width) + term.black_on_green( term.center(_("Available (%)"), width=col_width - 1) ) ) print( term.move(0, 3 * col_width) + term.black_on_green( term.center(_("Available (MB)"), width=col_width - 1) ) ) print( term.move(0, 4 * col_width) + term.black_on_green(term.center(_("Total (MB)"), width=col_width - 1)) ) print( term.move(0, 5 * col_width) + term.black_on_green( term.center(_("Sentinel (MB)"), width=col_width - 1) ) ) print( term.move(0, 6 * col_width) + term.black_on_green( term.center(_("Monitor (MB)"), width=col_width - 1) ) ) print( term.move(0, 7 * col_width) + term.black_on_green( term.center(_("Workers (MB)"), width=col_width - 1) ) ) row = 2 stats = Stat.get_all(broker=broker) print(term.clear_eos()) for stat in stats: # memory available (%) memory_available_percentage = round( psutil.virtual_memory().available * 100 / psutil.virtual_memory().total, 2, ) # memory available (MB) memory_available = round(psutil.virtual_memory().available / 1024**2, 2) if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE: MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now() print( term.move(row, 0 * col_width) + term.center(stat.host[: col_width - 1], width=col_width - 1) ) print( term.move(row, 1 * col_width) + term.center(str(stat.cluster_id)[-8:], width=col_width - 1) ) print( term.move(row, 2 * col_width) + term.center(memory_available_percentage, width=col_width - 1) ) print( term.move(row, 3 * col_width) + term.center(memory_available, width=col_width - 1) ) print( term.move(row, 4 * col_width) + term.center( round(psutil.virtual_memory().total / 1024**2, 2), width=col_width - 1, ) ) print( term.move(row, 5 * col_width) + term.center(get_process_mb(stat.sentinel), width=col_width - 1) ) print( term.move(row, 6 * col_width) + term.center( get_process_mb(getattr(stat, "monitor", None)), width=col_width - 1, ) ) workers_mb = 0 for worker_pid in stat.workers: result = get_process_mb(worker_pid) if isinstance(result, str): result = 0 workers_mb += result print( term.move(row, 7 * col_width) + term.center( workers_mb or "NO_PROCESSES_FOUND", width=col_width - 1 ) ) row += 1 # each worker's memory usage if workers: row += 2 col_width = int(term.width / (1 + Conf.WORKERS)) print( term.move(row, 0 * col_width) + term.black_on_cyan(term.center(_("Id"), width=col_width - 1)) ) for worker_num in range(Conf.WORKERS): print( term.move(row, (worker_num + 1) * col_width) + term.black_on_cyan( term.center( "Worker #{} (MB)".format(worker_num + 1), width=col_width - 1, ) ) ) row += 2 for stat in stats: print( term.move(row, 0 * col_width) + term.center(str(stat.cluster_id)[-8:], width=col_width - 1) ) for idx, worker_pid in enumerate(stat.workers): mb_used = get_process_mb(worker_pid) print( term.move(row, (idx + 1) * col_width) + term.center(mb_used, width=col_width - 1) ) row += 1 row += 1 print( term.move(row, 0) + _("Available lowest (): %(memory_percent)s ((at)s)") % { "memory_percent": str(MEMORY_AVAILABLE_LOWEST_PERCENTAGE), "at": MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT.strftime( "%Y-%m-%d %H:%M:%S+00:00" ), } ) # for testing if run_once: return Stat.get_all(broker=broker) print(term.move(row + 2, 0) + term.center(_("[Press q to quit]"))) val = term.inkey(timeout=1) def get_ids(): # prints id (PID) of running clusters stat = Stat.get_all() if stat: for s in stat: print(s.cluster_id) else: print(_("No clusters appear to be running.")) return True django-q2-1.7.4/django_q/pusher.py000066400000000000000000000043541471170400300167520ustar00rootroot00000000000000from multiprocessing import Event from multiprocessing.process import current_process from multiprocessing.queues import Queue from time import sleep from django import core from django.apps.registry import apps from django.utils.translation import gettext_lazy as _ try: apps.check_apps_ready() except core.exceptions.AppRegistryNotReady: import django django.setup() from django_q.brokers import Broker, get_broker from django_q.conf import Conf, logger from django_q.signing import BadSignature, SignedPackage try: import setproctitle except ModuleNotFoundError: setproctitle = None def pusher(task_queue: Queue, event: Event, broker: Broker = None): """ Pulls tasks of the broker and puts them in the task queue :type broker: :type task_queue: multiprocessing.Queue :type event: multiprocessing.Event """ if not broker: broker = get_broker() proc_name = current_process().name if setproctitle: setproctitle.setproctitle(f"qcluster {proc_name} pusher") logger.info( _("%(name)s pushing tasks at %(id)s") % {"name": proc_name, "id": current_process().pid} ) while True: try: task_set = broker.dequeue() except Exception: logger.exception("Failed to pull task from broker") # broker probably crashed. Let the sentinel handle it. sleep(10) break if task_set: for task in task_set: ack_id = task[0] # unpack the task try: task = SignedPackage.loads(task[1]) except (TypeError, BadSignature): logger.exception("Failed to push task to queue") broker.fail(ack_id) continue task["cluster"] = ( Conf.CLUSTER_NAME ) # save actual cluster name to orm task table task["ack_id"] = ack_id task_queue.put(task) logger.debug( _("queueing from %(list_key)s") % {"list_key": broker.list_key} ) if event.is_set(): break logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name}) django-q2-1.7.4/django_q/queues.py000066400000000000000000000055321471170400300167520ustar00rootroot00000000000000""" The code is derived from https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b """ import multiprocessing import multiprocessing.queues import sys class SharedCounter: """A synchronized shared counter. The locking done by multiprocessing.Value ensures that only a single process or thread may read or write the in-memory ctypes object. However, in order to do n += 1, Python performs a read followed by a write, so a second process may read the old value before the new one is written by the first process. The solution is to use a multiprocessing.Lock to guarantee the atomicity of the modifications to Value. This class comes almost entirely from Eli Bendersky's blog: http://eli.thegreenplace.net/2012/01/04/shared-counter-with-pythons-multiprocessing/ """ def __init__(self, n=0): self.count = multiprocessing.Value("i", n) def increment(self, n=1): """Increment the counter by n (default = 1)""" with self.count.get_lock(): self.count.value += n @property def value(self): """Return the value of the counter""" return self.count.value class Queue(multiprocessing.queues.Queue): """A portable implementation of multiprocessing.Queue. Because of multithreading / multiprocessing semantics, Queue.qsize() may raise the NotImplementedError exception on Unix platforms like Mac OS X where sem_getvalue() is not implemented. This subclass addresses this problem by using a synchronized shared counter (initialized to zero) and increasing / decreasing its value every time the put() and get() methods are called, respectively. This not only prevents NotImplementedError from being raised, but also allows us to implement a reliable version of both qsize() and empty(). """ def __init__(self, *args, **kwargs): if sys.version_info < (3, 0): super(Queue, self).__init__(*args, **kwargs) else: super(Queue, self).__init__( *args, ctx=multiprocessing.get_context(), **kwargs ) self.size = SharedCounter(0) def __getstate__(self): return super(Queue, self).__getstate__() + (self.size,) def __setstate__(self, state): super(Queue, self).__setstate__(state[:-1]) self.size = state[-1] def put(self, *args, **kwargs): super(Queue, self).put(*args, **kwargs) self.size.increment(1) def get(self, *args, **kwargs): x = super(Queue, self).get(*args, **kwargs) self.size.increment(-1) return x def qsize(self) -> int: """Reliable implementation of multiprocessing.Queue.qsize()""" return self.size.value def empty(self) -> bool: """Reliable implementation of multiprocessing.Queue.empty()""" return not self.qsize() > 0 django-q2-1.7.4/django_q/scheduler.py000066400000000000000000000126751471170400300174270ustar00rootroot00000000000000import ast from multiprocessing.process import current_process from django import core, db from django.apps.registry import apps from django.utils import timezone from django.utils.translation import gettext_lazy as _ try: apps.check_apps_ready() except core.exceptions.AppRegistryNotReady: import django django.setup() from django_q.brokers import Broker, get_broker from django_q.conf import Conf, logger from django_q.humanhash import humanize from django_q.models import Schedule from django_q.tasks import async_task from django_q.utils import close_old_django_connections, localtime def scheduler(broker: Broker = None): """ Creates a task from a schedule at the scheduled time and schedules next run """ if not broker: broker = get_broker() close_old_django_connections() try: # Only default cluster will handler schedule with default(null) cluster Q_default = ( db.models.Q(cluster__isnull=True) if Conf.CLUSTER_NAME == Conf.PREFIX else db.models.Q(pk__in=[]) ) with db.transaction.atomic(using=db.router.db_for_write(Schedule)): for s in ( Schedule.objects.select_for_update() .exclude(repeats=0) .filter(next_run__lt=timezone.now()) .filter(Q_default | db.models.Q(cluster=Conf.CLUSTER_NAME)) ): args = () kwargs = {} # get args, kwargs and hook if s.kwargs: try: # first try the dict syntax kwargs = ast.literal_eval(s.kwargs) except (SyntaxError, ValueError): # else use the kwargs syntax try: parsed_kwargs = ( ast.parse(f"f({s.kwargs})").body[0].value.keywords ) kwargs = { kwarg.arg: ast.literal_eval(kwarg.value) for kwarg in parsed_kwargs } except (SyntaxError, ValueError): kwargs = {} if s.args: args = ast.literal_eval(s.args) # single value won't eval to tuple, so: if type(args) is not tuple: args = (args,) q_options = kwargs.get("q_options", {}) if s.intended_date_kwarg: kwargs[s.intended_date_kwarg] = s.next_run.isoformat() if s.hook: q_options["hook"] = s.hook # set up the next run time if s.schedule_type != s.ONCE: next_run = s.next_run while True: next_run = s.calculate_next_run(next_run) if Conf.CATCH_UP or next_run > localtime(): break s.next_run = next_run # Little Fix for already broken numbers if s.repeats < -1: s.repeats = -1 # Check if the value is not zero if s.repeats > 0: s.repeats -= 1 # send it to the cluster; any cluster name is allowed in multi-queue scenarios # because `broker_name` is confusing, using `cluster` name is recommended and takes precedence q_options["cluster"] = s.cluster or q_options.get( "cluster", q_options.pop("broker_name", None) ) if ( q_options["cluster"] is None or q_options["cluster"] == Conf.CLUSTER_NAME ): q_options["broker"] = broker q_options["group"] = q_options.get("group", s.name or s.id) kwargs["q_options"] = q_options s.task = async_task(s.func, *args, **kwargs) # log it if not s.task: logger.error( _( "%(process_name)s failed to create a task from schedule " "[%(schedule)s]" ) % { "process_name": current_process().name, "schedule": s.name or s.id, } ) else: logger.info( _( "%(process_name)s created task %(task_name)s from schedule " "[%(schedule)s]" ) % { "process_name": current_process().name, "task_name": humanize(s.task), "schedule": s.name or s.id, } ) # default behavior is to delete a ONCE schedule if s.schedule_type == s.ONCE: if s.repeats < 0: s.delete() continue # but not if it has a positive repeats s.repeats = 0 # save the schedule s.save() except Exception: logger.exception("Could not create task from schedule") django-q2-1.7.4/django_q/signals.py000066400000000000000000000023371471170400300171030ustar00rootroot00000000000000import importlib from django.db.models.signals import post_save from django.dispatch import Signal, receiver from django.utils.translation import gettext_lazy as _ from django_q.conf import logger from django_q.models import Task @receiver(post_save, sender=Task) def call_hook(sender, instance, **kwargs): if instance.hook: f = instance.hook if not callable(f): try: module, func = f.rsplit(".", 1) m = importlib.import_module(module) f = getattr(m, func) except (ValueError, ImportError, AttributeError): logger.error( _("malformed return hook '%(hook)s' for [%(name)s]") % {"hook": instance.hook, "name": instance.name} ) return try: f(instance) except Exception as e: logger.error( _("return hook %(hook)s failed on [%(name)s] because %(error)s") % {"hook": instance.hook, "name": instance.name, "error": str(e)} ) # args: proc_name post_spawn = Signal() # args: task pre_enqueue = Signal() # args: func, task pre_execute = Signal() # args: task post_execute = Signal() django-q2-1.7.4/django_q/signing.py000066400000000000000000000017361471170400300171030ustar00rootroot00000000000000"""Package signing.""" import pickle from django_q import core_signing as signing from django_q.conf import Conf BadSignature = signing.BadSignature class SignedPackage: """Wraps Django's signing module with custom Pickle serializer.""" @staticmethod def dumps(obj, compressed: bool = Conf.COMPRESSED) -> str: return signing.dumps( obj, key=Conf.SECRET_KEY, salt=Conf.PREFIX, compress=compressed, serializer=PickleSerializer, ) @staticmethod def loads(obj) -> any: return signing.loads( obj, key=Conf.SECRET_KEY, salt=Conf.PREFIX, serializer=PickleSerializer ) class PickleSerializer: """Simple wrapper around Pickle for signing.dumps and signing.loads.""" @staticmethod def dumps(obj) -> bytes: return pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) @staticmethod def loads(data) -> any: return pickle.loads(data) django-q2-1.7.4/django_q/status.py000066400000000000000000000070601471170400300167640ustar00rootroot00000000000000import socket from typing import Union from django.utils import timezone from django_q.brokers import Broker, get_broker from django_q.conf import Conf, logger from django_q.signing import BadSignature, SignedPackage class Status: """Cluster status base class.""" def __init__(self, pid, cluster_id): self.workers = [] self.tob = None self.reincarnations = 0 self.pid = pid self.cluster_id = cluster_id self.sentinel = 0 self.status = Conf.STOPPED self.done_q_size = 0 self.host = socket.gethostname() self.monitor = 0 self.task_q_size = 0 self.pusher = 0 self.timestamp = timezone.now() class Stat(Status): """Status object for Cluster monitoring.""" def __init__(self, sentinel): super(Stat, self).__init__( sentinel.parent_pid or sentinel.pid, cluster_id=sentinel.cluster_id ) self.broker = sentinel.broker or get_broker() self.tob = sentinel.tob self.reincarnations = sentinel.reincarnations self.sentinel = sentinel.pid self.status = sentinel.status() self.done_q_size = 0 self.task_q_size = 0 if Conf.QSIZE: self.done_q_size = sentinel.result_queue.qsize() self.task_q_size = sentinel.task_queue.qsize() if sentinel.monitor: self.monitor = sentinel.monitor.pid if sentinel.pusher: self.pusher = sentinel.pusher.pid self.workers = [w.pid for w in sentinel.pool] def uptime(self) -> float: return (timezone.now() - self.tob).total_seconds() @property def key(self) -> str: """ :return: redis key for this cluster statistic """ return self.get_key(self.cluster_id) @staticmethod def get_key(cluster_id) -> str: """ :param cluster_id: cluster ID :return: redis key for the cluster statistic """ return f"{Conf.Q_STAT}:{cluster_id}" def save(self): try: self.broker.set_stat(self.key, SignedPackage.dumps(self, True), 3) except Exception as e: logger.error(e) def empty_queues(self) -> bool: return self.done_q_size + self.task_q_size == 0 @staticmethod def get(pid: int, cluster_id: str, broker: Broker = None) -> Union[Status, None]: """ gets the current status for the cluster :param pid: :param broker: an optional broker instance :param cluster_id: id of the cluster :return: Stat or Status """ if not broker: broker = get_broker() pack = broker.get_stat(Stat.get_key(cluster_id)) if pack: try: return SignedPackage.loads(pack) except BadSignature: return None return Status(pid=pid, cluster_id=cluster_id) @staticmethod def get_all(broker: Broker = None) -> list: """ Get the status for all currently running clusters with the same prefix and secret key. :return: list of type Stat """ if not broker: broker = get_broker() stats = [] packs = broker.get_stats(f"{Conf.Q_STAT}:*") or [] for pack in packs: try: stats.append(SignedPackage.loads(pack)) except BadSignature: continue return stats def __getstate__(self): # Don't pickle the redis connection state = dict(self.__dict__) del state["broker"] return state django-q2-1.7.4/django_q/tasks.py000066400000000000000000000532751471170400300165770ustar00rootroot00000000000000"""Provides task functionality.""" # Standard from multiprocessing import Value from time import sleep, time # django from django.db import IntegrityError from django.utils import timezone # local from django_q.brokers import get_broker from django_q.conf import Conf, logger from django_q.humanhash import uuid from django_q.models import Schedule, Task from django_q.queues import Queue from django_q.signals import pre_enqueue from django_q.signing import SignedPackage def async_task(func, *args, **kwargs): """Queue a task for the cluster.""" keywords = kwargs.copy() opt_keys = ( "hook", "group", "save", "sync", "cached", "ack_failure", "iter_count", "iter_cached", "chain", "broker", "cluster", "timeout", ) q_options = keywords.pop("q_options", {}) # get an id tag = uuid() # build the task package task = { "id": tag[1], "name": keywords.pop("task_name", None) or q_options.pop("task_name", None) or tag[0], "func": func, "args": args, } # push optionals for key in opt_keys: if q_options and key in q_options: task[key] = q_options[key] elif key in keywords: task[key] = keywords.pop(key) # don't serialize the broker broker = task.pop("broker", None) or get_broker(task.get("cluster")) # overrides if "cached" not in task and Conf.CACHED: task["cached"] = Conf.CACHED if "sync" not in task and Conf.SYNC: task["sync"] = Conf.SYNC if "ack_failure" not in task and Conf.ACK_FAILURES: task["ack_failure"] = Conf.ACK_FAILURES # finalize task["kwargs"] = keywords task["started"] = timezone.now() # signal it pre_enqueue.send(sender="django_q", task=task) # sign it pack = SignedPackage.dumps(task) if task.get("sync", False): return _sync(pack) # push it enqueue_id = broker.enqueue(pack) logger.info(f"Enqueued [{broker.list_key}] {enqueue_id}") logger.debug(f"Pushed {tag}") return task["id"] def schedule(func, *args, **kwargs): """ Create a schedule. :param func: function to schedule. :param args: function arguments. :param name: optional name for the schedule. :param hook: optional result hook function. :type schedule_type: Schedule.TYPE :param repeats: how many times to repeat. 0=never, -1=always. :param next_run: Next scheduled run. :type next_run: datetime.datetime :param cluster: optional cluster name. :param cron: optional cron expression :param intended_date_kwarg: optional identifier to pass intended schedule date. :param kwargs: function keyword arguments. :return: the schedule object. :rtype: Schedule """ name = kwargs.pop("name", None) hook = kwargs.pop("hook", None) schedule_type = kwargs.pop("schedule_type", Schedule.ONCE) minutes = kwargs.pop("minutes", None) repeats = kwargs.pop("repeats", -1) next_run = kwargs.pop("next_run", timezone.now()) cron = kwargs.pop("cron", None) cluster = kwargs.pop("cluster", None) intended_date_kwarg = kwargs.pop("intended_date_kwarg", None) # check for name duplicates instead of am unique constraint if name and Schedule.objects.filter(name=name).exists(): raise IntegrityError("A schedule with the same name already exists.") # create and return the schedule s = Schedule( name=name, func=func, hook=hook, args=args, kwargs=kwargs, schedule_type=schedule_type, minutes=minutes, repeats=repeats, next_run=next_run, cron=cron, cluster=cluster, intended_date_kwarg=intended_date_kwarg, ) # make sure we trigger validation s.full_clean() s.save() return s def result(task_id, wait=0, cached=Conf.CACHED): """ Return the result of the named task. :type task_id: str or uuid :param task_id: the task name or uuid :type wait: int :param wait: number of milliseconds to wait for a result :param bool cached: run this against the cache backend :return: the result object of this task :rtype: object """ if cached: return result_cached(task_id, wait) start = time() while True: r = Task.get_result(task_id) if r is not None: return r if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def result_cached(task_id, wait=0, broker=None): """ Return the result from the cache backend """ if not broker: broker = get_broker() start = time() while True: r = broker.cache.get(f"{broker.list_key}:{task_id}") if r: return SignedPackage.loads(r)["result"] if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def result_group(group_id, failures=False, wait=0, count=None, cached=Conf.CACHED): """ Return a list of results for a task group. :param str group_id: the group id :param bool failures: set to True to include failures :param int count: Block until there are this many results in the group :param bool cached: run this against the cache backend :return: list or results """ if cached: return result_group_cached(group_id, failures, wait, count) start = time() if count: while True: if ( count_group(group_id) == count or wait and (time() - start) * 1000 >= wait >= 0 ): break sleep(0.01) while True: r = Task.get_result_group(group_id, failures) if r: return r if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def result_group_cached(group_id, failures=False, wait=0, count=None, broker=None): """ Return a list of results for a task group from the cache backend """ if not broker: broker = get_broker() start = time() if count: while True: if ( count_group_cached(group_id) == count or wait and (time() - start) * 1000 >= wait > 0 ): break sleep(0.01) while True: group_list = broker.cache.get(f"{broker.list_key}:{group_id}:keys") if group_list: result_list = [] for task_key in group_list: task = SignedPackage.loads(broker.cache.get(task_key)) if task["success"] or failures: result_list.append(task["result"]) return result_list if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def fetch(task_id, wait=0, cached=Conf.CACHED): """ Return the processed task. :param task_id: the task name or uuid :type task_id: str or uuid :param wait: the number of milliseconds to wait for a result :type wait: int :param bool cached: run this against the cache backend :return: the full task object :rtype: Task """ if cached: return fetch_cached(task_id, wait) start = time() while True: t = Task.get_task(task_id) if t: return t if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def fetch_cached(task_id, wait=0, broker=None): """ Return the processed task from the cache backend """ if not broker: broker = get_broker() start = time() while True: r = broker.cache.get(f"{broker.list_key}:{task_id}") if r: task = SignedPackage.loads(r) return Task( id=task["id"], name=task["name"], func=task["func"], hook=task.get("hook"), args=task["args"], kwargs=task["kwargs"], cluster=task.get("cluster"), started=task["started"], stopped=task["stopped"], result=task["result"], success=task["success"], ) if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def fetch_group(group_id, failures=True, wait=0, count=None, cached=Conf.CACHED): """ Return a list of Tasks for a task group. :param str group_id: the group id :param bool failures: set to False to exclude failures :param bool cached: run this against the cache backend :return: list of Tasks """ if cached: return fetch_group_cached(group_id, failures, wait, count) start = time() if count: while True: if ( count_group(group_id) == count or wait and (time() - start) * 1000 >= wait >= 0 ): break sleep(0.01) while True: r = Task.get_task_group(group_id, failures) if r: return r if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None): """ Return a list of Tasks for a task group in the cache backend """ if not broker: broker = get_broker() start = time() if count: while True: if ( count_group_cached(group_id) == count or wait and (time() - start) * 1000 >= wait >= 0 ): break sleep(0.01) while True: group_list = broker.cache.get(f"{broker.list_key}:{group_id}:keys") if group_list: task_list = [] for task_key in group_list: task = SignedPackage.loads(broker.cache.get(task_key)) if task["success"] or failures: t = Task( id=task["id"], name=task["name"], func=task["func"], hook=task.get("hook"), args=task["args"], kwargs=task["kwargs"], cluster=task.get("cluster"), started=task["started"], stopped=task["stopped"], result=task["result"], group=task.get("group"), success=task["success"], ) task_list.append(t) return task_list if (time() - start) * 1000 >= wait >= 0: break sleep(0.01) def count_group(group_id, failures=False, cached=Conf.CACHED): """ Count the results in a group. :param str group_id: the group id :param bool failures: Returns failure count if True :param bool cached: run this against the cache backend :return: the number of tasks/results in a group :rtype: int """ if cached: return count_group_cached(group_id, failures) return Task.get_group_count(group_id, failures) def count_group_cached(group_id, failures=False, broker=None): """ Count the results in a group in the cache backend """ if not broker: broker = get_broker() group_list = broker.cache.get(f"{broker.list_key}:{group_id}:keys") if group_list: if not failures: return len(group_list) failure_count = 0 for task_key in group_list: task = SignedPackage.loads(broker.cache.get(task_key)) if not task["success"]: failure_count += 1 return failure_count def delete_group(group_id, tasks=False, cached=Conf.CACHED): """ Delete a group. :param str group_id: the group id :param bool tasks: If set to True this will also delete the group tasks. Otherwise just the group label is removed. :param bool cached: run this against the cache backend :return: """ if cached: return delete_group_cached(group_id) return Task.delete_group(group_id, tasks) def delete_group_cached(group_id, broker=None): """ Delete a group from the cache backend """ if not broker: broker = get_broker() group_key = f"{broker.list_key}:{group_id}:keys" group_list = broker.cache.get(group_key) broker.cache.delete_many(group_list) broker.cache.delete(group_key) def delete_cached(task_id, broker=None): """ Delete a task from the cache backend """ if not broker: broker = get_broker() return broker.cache.delete(f"{broker.list_key}:{task_id}") def queue_size(broker=None): """ Returns the current queue size. Note that this doesn't count any tasks currently being processed by workers. :param broker: optional broker :return: current queue size :rtype: int """ if not broker: broker = get_broker() return broker.queue_size() def async_iter(func, args_iter, **kwargs): """ enqueues a function with iterable arguments """ iter_count = len(args_iter) iter_group = uuid()[1] # clean up the kwargs options = kwargs.get("q_options", kwargs) options.pop("hook", None) options["broker"] = options.get("broker", get_broker()) options["group"] = iter_group options["iter_count"] = iter_count if options.get("cached", None): options["iter_cached"] = options["cached"] options["cached"] = True # save the original arguments broker = options["broker"] broker.cache.set( f"{broker.list_key}:{iter_group}:args", SignedPackage.dumps(args_iter) ) for args in args_iter: if not isinstance(args, tuple): args = (args,) async_task(func, *args, **options) return iter_group def async_chain(chain, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None): """ enqueues a chain of tasks the chain must be in the format [(func,(args),{kwargs}),(func,(args),{kwargs})] """ if not group: group = uuid()[1] args = () kwargs = {} task = chain.pop(0) if type(task) is not tuple: task = (task,) if len(task) > 1: args = task[1] if len(task) > 2: kwargs = task[2] kwargs["chain"] = chain kwargs["group"] = group kwargs["cached"] = cached kwargs["sync"] = sync kwargs["broker"] = broker or get_broker() async_task(task[0], *args, **kwargs) return group class Iter: """ An async task with iterable arguments """ def __init__( self, func=None, args=None, kwargs=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None, ): self.func = func self.args = args or [] self.kwargs = kwargs or {} self.id = "" self.broker = broker or get_broker() self.cached = cached self.sync = sync self.started = False def append(self, *args): """ add arguments to the set """ self.args.append(args) if self.started: self.started = False return self.length() def run(self): """ Start queueing the tasks to the worker cluster :return: the task id """ self.kwargs["cached"] = self.cached self.kwargs["sync"] = self.sync self.kwargs["broker"] = self.broker self.id = async_iter(self.func, self.args, **self.kwargs) self.started = True return self.id def result(self, wait=0): """ return the full list of results. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of results """ if self.started: return result(self.id, wait=wait, cached=self.cached) def fetch(self, wait=0): """ get the task result objects. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of task objects """ if self.started: return fetch(self.id, wait=wait, cached=self.cached) def length(self): """ get the length of the arguments list :return int: length of the argument list """ return len(self.args) class Chain: """ A sequential chain of tasks """ def __init__( self, chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None ): self.chain = chain or [] self.group = group or "" self.broker = broker or get_broker() self.cached = cached self.sync = sync self.started = False def append(self, func, *args, **kwargs): """ add a task to the chain takes the same parameters as async_task() """ self.chain.append((func, args, kwargs)) # remove existing results if self.started: delete_group(self.group) self.started = False return self.length() def run(self): """ Start queueing the chain to the worker cluster :return: the chain's group id """ self.group = async_chain( chain=self.chain[:], group=self.group, cached=self.cached, sync=self.sync, broker=self.broker, ) self.started = True return self.group def result(self, wait=0): """ return the full list of results from the chain when it finishes. blocks until timeout. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of results """ if self.started: return result_group( self.group, wait=wait, count=self.length(), cached=self.cached ) def fetch(self, failures=True, wait=0): """ get the task result objects from the chain when it finishes. blocks until timeout. :param failures: include failed tasks :param int wait: how many milliseconds to wait for a result :return: an unsorted list of task objects """ if self.started: return fetch_group( self.group, failures=failures, wait=wait, count=self.length(), cached=self.cached, ) def current(self): """ get the index of the currently executing chain element :return int: current chain index """ if not self.started: return None return count_group(self.group, cached=self.cached) def length(self): """ get the length of the chain :return int: length of the chain """ return len(self.chain) class AsyncTask: """ an async task """ def __init__(self, func, *args, **kwargs): self.id = "" self.started = False self.func = func self.args = args self.kwargs = kwargs @property def broker(self): return self._get_option("broker", None) @broker.setter def broker(self, value): self._set_option("broker", value) @property def sync(self): return self._get_option("sync", None) @sync.setter def sync(self, value): self._set_option("sync", value) @property def save(self): return self._get_option("save", None) @save.setter def save(self, value): self._set_option("save", value) @property def hook(self): return self._get_option("hook", None) @hook.setter def hook(self, value): self._set_option("hook", value) @property def group(self): return self._get_option("group", None) @group.setter def group(self, value): self._set_option("group", value) @property def cached(self): return self._get_option("cached", Conf.CACHED) @cached.setter def cached(self, value): self._set_option("cached", value) def _set_option(self, key, value): if "q_options" in self.kwargs: self.kwargs["q_options"][key] = value else: self.kwargs[key] = value self.started = False def _get_option(self, key, default=None): if "q_options" in self.kwargs: return self.kwargs["q_options"].get(key, default) else: return self.kwargs.get(key, default) def run(self): self.id = async_task(self.func, *self.args, **self.kwargs) self.started = True return self.id def result(self, wait=0): if self.started: return result(self.id, wait=wait, cached=self.cached) def fetch(self, wait=0): if self.started: return fetch(self.id, wait=wait, cached=self.cached) def result_group(self, failures=False, wait=0, count=None): if self.started and self.group: return result_group( self.group, failures=failures, wait=wait, count=count, cached=self.cached, ) def fetch_group(self, failures=True, wait=0, count=None): if self.started and self.group: return fetch_group( self.group, failures=failures, wait=wait, count=count, cached=self.cached, ) def _sync(pack): """Simulate a package travelling through the cluster.""" from django_q.monitor import monitor from django_q.worker import worker task_queue = Queue() result_queue = Queue() task = SignedPackage.loads(pack) task_queue.put(task) task_queue.put("STOP") worker(task_queue, result_queue, Value("f", -1)) result_queue.put("STOP") monitor(result_queue) task_queue.close() task_queue.join_thread() result_queue.close() result_queue.join_thread() return task["id"] django-q2-1.7.4/django_q/tests/000077500000000000000000000000001471170400300162265ustar00rootroot00000000000000django-q2-1.7.4/django_q/tests/__init__.py000066400000000000000000000000001471170400300203250ustar00rootroot00000000000000django-q2-1.7.4/django_q/tests/settings.py000066400000000000000000000060351471170400300204440ustar00rootroot00000000000000import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = ")cqmpi+p@n&!u&fu@!m@9h&1bz9mwmstsahe)nf!ms+c$uc=x7" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_q", "django_redis", ) MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ) MIDDLEWARE = MIDDLEWARE_CLASSES ROOT_URLCONF = "tests.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "Europe/Amsterdam" USE_I18N = True USE_L10N = True USE_TZ = True LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": { "console": { "class": "logging.StreamHandler", }, }, "loggers": { "django_q": { "handlers": ["console"], "level": "INFO", }, }, } # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = "/static/" REDIS_HOST = os.environ.get("REDIS_HOST", "redis") MONGO_HOST = os.environ.get("MONGO_HOST", "mongo") # Django Redis CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": f"redis://{REDIS_HOST}:6379/0", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PARSER_CLASS": "redis.connection.HiredisParser", }, } } # Django Q specific Q_CLUSTER = { "name": "django_q_test", "cpu_affinity": 1, "testing": True, "log_level": "DEBUG", "django_redis": "default", "redis": f"redis://{REDIS_HOST}:6379/0", } django-q2-1.7.4/django_q/tests/tasks.py000066400000000000000000000013621471170400300177270ustar00rootroot00000000000000from time import sleep class TaskError(Exception): pass def countdown(n): while n > 0: n -= 1 def multiply(x, y): return x * y def count_letters(tup): total = 0 for word in tup: total += len(word) return total def count_letters2(obj): return count_letters(obj.get_words()) def word_multiply(x, word=""): return len(word) * x def count_forever(): while True: sleep(0.5) def get_task_name(task): return task.name def get_user_id(user): return user.id def hello(): return "hello" def return_falsy_value(): return [] def result(obj): print(f"RESULT HOOK {obj.name} : {obj.result()}") def raise_exception(): raise TaskError("this is an exception!") django-q2-1.7.4/django_q/tests/test_admin.py000066400000000000000000000064371471170400300207410ustar00rootroot00000000000000import pytest from django.urls import reverse from django.utils import timezone from django_q.conf import Conf from django_q.humanhash import uuid from django_q.models import Failure, OrmQ, Task from django_q.signing import SignedPackage from django_q.tasks import schedule @pytest.mark.django_db def test_admin_views(admin_client, monkeypatch): monkeypatch.setattr(Conf, "ORM", "default") s = schedule("schedule.test") tag = uuid() f = Task.objects.create( id=tag[1], name=tag[0], func="test.fail", started=timezone.now(), stopped=timezone.now(), success=False, ) tag = uuid() t = Task.objects.create( id=tag[1], name=tag[0], func="test.success", started=timezone.now(), stopped=timezone.now(), success=True, ) q = OrmQ.objects.create( key="test", payload=SignedPackage.dumps({"id": 1, "func": "test", "name": "test"}), ) admin_urls = ( # schedule reverse("admin:django_q_schedule_changelist"), reverse("admin:django_q_schedule_add"), reverse("admin:django_q_schedule_change", args=(s.id,)), reverse("admin:django_q_schedule_history", args=(s.id,)), reverse("admin:django_q_schedule_delete", args=(s.id,)), # success reverse("admin:django_q_success_changelist"), reverse("admin:django_q_success_change", args=(t.id,)), reverse("admin:django_q_success_history", args=(t.id,)), reverse("admin:django_q_success_delete", args=(t.id,)), # failure reverse("admin:django_q_failure_changelist"), reverse("admin:django_q_failure_change", args=(f.id,)), reverse("admin:django_q_failure_history", args=(f.id,)), reverse("admin:django_q_failure_delete", args=(f.id,)), # orm queue reverse("admin:django_q_ormq_changelist"), reverse("admin:django_q_ormq_change", args=(q.id,)), reverse("admin:django_q_ormq_history", args=(q.id,)), reverse("admin:django_q_ormq_delete", args=(q.id,)), ) for url in admin_urls: response = admin_client.get(url) assert response.status_code == 200 # resubmit the failure url = reverse("admin:django_q_failure_changelist") data = {"action": "resubmit_task", "_selected_action": [f.pk]} response = admin_client.post(url, data) assert response.status_code == 302 assert Failure.objects.filter(name=f.id).exists() is False # change q url = reverse("admin:django_q_ormq_change", args=(q.id,)) data = { "key": "default", "payload": "test", "lock_0": "2015-09-17", "lock_1": "14:31:51", "_save": "Save", } response = admin_client.post(url, data) assert response.status_code == 302 # delete q url = reverse("admin:django_q_ormq_delete", args=(q.id,)) data = {"post": "yes"} response = admin_client.post(url, data) assert response.status_code == 302 # Resubmit a successful task. url = reverse("admin:django_q_success_changelist") data = {"action": "resubmit_task", "_selected_action": [t.pk]} initial_queue_count = OrmQ.objects.count() response = admin_client.post(url, data) assert response.status_code == 302 assert OrmQ.objects.count() > initial_queue_count django-q2-1.7.4/django_q/tests/test_brokers.py000066400000000000000000000210701471170400300213060ustar00rootroot00000000000000import os from time import sleep import pytest from django_q.brokers import Broker, get_broker from django_q.conf import Conf from django_q.humanhash import uuid from django_q.tests.settings import MONGO_HOST, REDIS_HOST def test_broker(monkeypatch): broker = Broker() broker.enqueue("test") broker.dequeue() broker.queue_size() broker.lock_size() broker.purge_queue() broker.delete("id") broker.delete_queue() broker.acknowledge("test") broker.ping() broker.info() # stats assert broker.get_stat("test_1") is None broker.set_stat("test_1", "test", 3) assert broker.get_stat("test_1") == "test" assert broker.get_stats("test:*")[0] == "test" # stats with no cache monkeypatch.setattr(Conf, "CACHE", "not_configured") broker.cache = broker.get_cache() assert broker.get_stat("test_1") is None broker.set_stat("test_1", "test", 3) assert broker.get_stat("test_1") is None assert broker.get_stats("test:*") is None def test_redis(monkeypatch): monkeypatch.setattr(Conf, "DJANGO_REDIS", None) broker = get_broker() assert broker.ping() is True assert broker.info() is not None monkeypatch.setattr(Conf, "REDIS", {"host": REDIS_HOST, "port": 7799}) broker = get_broker() with pytest.raises(Exception): broker.ping() monkeypatch.setattr(Conf, "REDIS", f"redis://{REDIS_HOST}:7799") broker = get_broker() with pytest.raises(Exception): broker.ping() def test_custom(monkeypatch): monkeypatch.setattr(Conf, "BROKER_CLASS", "django_q.brokers.redis_broker.Redis") broker = get_broker() assert broker.ping() is True assert broker.info() is not None assert broker.__class__.__name__ == "Redis" @pytest.mark.skipif( not os.getenv("IRON_MQ_TOKEN"), reason="requires IronMQ credentials" ) def test_ironmq(monkeypatch): monkeypatch.setattr( Conf, "IRON_MQ", { "token": os.getenv("IRON_MQ_TOKEN"), "project_id": os.getenv("IRON_MQ_PROJECT_ID"), }, ) # check broker broker = get_broker(list_key=uuid()[0]) assert broker.ping() is True assert broker.info() is not None # initialize the queue broker.enqueue("test") # clear before we start broker.purge_queue() assert broker.queue_size() == 0 # async_task broker.enqueue("test") # dequeue task = broker.dequeue()[0] assert task[1] == "test" broker.acknowledge(task[0]) assert broker.dequeue() is None # Retry test # monkeypatch.setattr(Conf, 'RETRY', 1) # broker.async_task('test') # assert broker.dequeue() is not None # sleep(3) # assert broker.dequeue() is not None # task = broker.dequeue()[0] # assert len(task) > 0 # broker.acknowledge(task[0]) # sleep(3) # delete job task_id = broker.enqueue("test") broker.delete(task_id) assert broker.dequeue() is None # fail task_id = broker.enqueue("test") broker.fail(task_id) # bulk test for _ in range(5): broker.enqueue("test") monkeypatch.setattr(Conf, "BULK", 5) tasks = broker.dequeue() for task in tasks: assert task is not None broker.acknowledge(task[0]) # duplicate acknowledge broker.acknowledge(task[0]) # delete queue broker.enqueue("test") broker.enqueue("test") broker.purge_queue() assert broker.dequeue() is None broker.delete_queue() @pytest.mark.skipif( not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials" ) def test_sqs(monkeypatch): monkeypatch.setattr( Conf, "SQS", { "aws_region": os.getenv("AWS_REGION"), "aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"), "receive_message_wait_time_seconds": 5, }, ) # check broker broker = get_broker(list_key="testing") assert "receive_message_wait_time_seconds" in Conf.SQS assert "aws_region" in Conf.SQS assert broker.ping() is True assert broker.info() is not None assert broker.queue_size() == 0 # async_task broker.enqueue("test") # dequeue task = broker.dequeue()[0] assert task[1] == "test" broker.acknowledge(task[0]) assert broker.dequeue() is None # Retry test monkeypatch.setattr(Conf, "RETRY", 1) broker.enqueue("test") sleep(2) # Sometimes SQS is not linear task = broker.dequeue() if not task: pytest.skip("SQS being weird") task = task[0] assert len(task) > 0 broker.acknowledge(task[0]) sleep(2) # delete job monkeypatch.setattr(Conf, "RETRY", 60) broker.enqueue("test") sleep(1) task = broker.dequeue() if not task: pytest.skip("SQS being weird") task_id = task[0][0] broker.delete(task_id) assert broker.dequeue() is None # fail broker.enqueue("test") while task is None: task = broker.dequeue()[0] broker.fail(task[0][0]) # bulk test for _ in range(10): broker.enqueue("test") monkeypatch.setattr(Conf, "BULK", 12) tasks = broker.dequeue() for task in tasks: assert task is not None broker.acknowledge(task[0]) # duplicate acknowledge broker.acknowledge(task[0]) assert broker.lock_size() == 0 # delete queue broker.enqueue("test") broker.purge_queue() broker.delete_queue() @pytest.mark.django_db def test_orm(monkeypatch): monkeypatch.setattr(Conf, "ORM", "default") # check broker broker = get_broker(list_key="orm_test") assert broker.ping() is True assert broker.info() is not None # clear before we start broker.delete_queue() # async_task broker.enqueue("test") assert broker.queue_size() == 1 # dequeue task = broker.dequeue()[0] assert task[1] == "test" broker.acknowledge(task[0]) assert broker.queue_size() == 0 # Retry test monkeypatch.setattr(Conf, "RETRY", 1) broker.enqueue("test") assert broker.queue_size() == 1 broker.dequeue() assert broker.queue_size() == 0 sleep(1.5) assert broker.queue_size() == 1 task = broker.dequeue()[0] assert broker.queue_size() == 0 broker.acknowledge(task[0]) sleep(1.5) assert broker.queue_size() == 0 # delete job task_id = broker.enqueue("test") broker.delete(task_id) assert broker.dequeue() is None # fail task_id = broker.enqueue("test") broker.fail(task_id) # bulk test for _ in range(5): broker.enqueue("test") monkeypatch.setattr(Conf, "BULK", 5) tasks = broker.dequeue() assert broker.lock_size() == Conf.BULK for task in tasks: assert task is not None broker.acknowledge(task[0]) # test lock size assert broker.lock_size() == 0 # test duplicate acknowledge broker.acknowledge(task[0]) # delete queue broker.enqueue("test") broker.enqueue("test") broker.delete_queue() assert broker.queue_size() == 0 @pytest.mark.django_db def test_mongo(monkeypatch): monkeypatch.setattr(Conf, "MONGO", {"host": MONGO_HOST, "port": 27017}) # check broker broker = get_broker(list_key="mongo_test") assert broker.ping() is True assert broker.info() is not None # clear before we start broker.delete_queue() # async_task broker.enqueue("test") assert broker.queue_size() == 1 # dequeue task = broker.dequeue()[0] assert task[1] == "test" broker.acknowledge(task[0]) assert broker.queue_size() == 0 # Retry test monkeypatch.setattr(Conf, "RETRY", 1) broker.enqueue("test") assert broker.queue_size() == 1 broker.dequeue() assert broker.queue_size() == 0 sleep(1.5) assert broker.queue_size() == 1 task = broker.dequeue()[0] assert broker.queue_size() == 0 broker.acknowledge(task[0]) sleep(1.5) assert broker.queue_size() == 0 # delete job task_id = broker.enqueue("test") broker.delete(task_id) assert broker.dequeue() is None # fail task_id = broker.enqueue("test") broker.fail(task_id) # bulk test for _ in range(5): broker.enqueue("test") tasks = [broker.dequeue()[0] for _ in range(5)] assert broker.lock_size() == 5 for task in tasks: assert task is not None broker.acknowledge(task[0]) # test lock size assert broker.lock_size() == 0 # test duplicate acknowledge broker.acknowledge(task[0]) # delete queue broker.enqueue("test") broker.enqueue("test") broker.purge_queue() broker.delete_queue() assert broker.queue_size() == 0 django-q2-1.7.4/django_q/tests/test_cached.py000066400000000000000000000153221471170400300210510ustar00rootroot00000000000000from multiprocessing import Event, Value import pytest from django_q.brokers import get_broker from django_q.conf import Conf from django_q.monitor import monitor from django_q.pusher import pusher from django_q.queues import Queue from django_q.tasks import ( AsyncTask, Chain, Iter, async_chain, async_iter, async_task, count_group, delete_cached, delete_group, fetch, fetch_group, result, result_group, ) from django_q.worker import worker @pytest.fixture def broker(monkeypatch): monkeypatch.setattr(Conf, "DJANGO_REDIS", "default") return get_broker() @pytest.mark.django_db def test_cached(broker): broker.purge_queue() broker.cache.clear() group = "cache_test" # queue the tests task_id = async_task("math.copysign", 1, -1, cached=True, broker=broker) async_task("math.copysign", 1, -1, cached=True, broker=broker, group=group) async_task("math.copysign", 1, -1, cached=True, broker=broker, group=group) async_task("math.copysign", 1, -1, cached=True, broker=broker, group=group) async_task("math.copysign", 1, -1, cached=True, broker=broker, group=group) async_task("math.copysign", 1, -1, cached=True, broker=broker, group=group) async_task("math.popysign", 1, -1, cached=True, broker=broker, group=group) iter_id = async_iter("math.floor", [i for i in range(10)], cached=True) # test wait on cache # test wait timeout assert result(task_id, wait=10, cached=True) is None assert fetch(task_id, wait=10, cached=True) is None assert result_group(group, wait=10, cached=True) is None assert result_group(group, count=2, wait=10, cached=True) is None assert fetch_group(group, wait=10, cached=True) is None assert fetch_group(group, count=2, wait=10, cached=True) is None # run a single inline cluster task_count = 17 assert broker.queue_size() == task_count task_queue = Queue() stop_event = Event() stop_event.set() for i in range(task_count): pusher(task_queue, stop_event, broker=broker) assert broker.queue_size() == 0 assert task_queue.qsize() == task_count task_queue.put("STOP") result_queue = Queue() worker(task_queue, result_queue, Value("f", -1)) assert result_queue.qsize() == task_count result_queue.put("STOP") monitor(result_queue) assert result_queue.qsize() == 0 # assert results assert result(task_id, wait=500, cached=True) == -1 assert fetch(task_id, wait=500, cached=True).result == -1 # make sure it's not in the db backend assert fetch(task_id) is None # assert group assert count_group(group, cached=True) == 6 assert count_group(group, cached=True, failures=True) == 1 assert result_group(group, cached=True) == [-1, -1, -1, -1, -1] assert len(result_group(group, cached=True, failures=True)) == 6 assert len(fetch_group(group, cached=True)) == 6 assert len(fetch_group(group, cached=True, failures=False)) == 5 delete_group(group, cached=True) assert count_group(group, cached=True) is None delete_cached(task_id) assert result(task_id, cached=True) is None assert fetch(task_id, cached=True) is None # iter cached assert result(iter_id) is None assert result(iter_id, cached=True) is not None broker.cache.clear() @pytest.mark.django_db def test_iter(broker): broker.purge_queue() broker.cache.clear() it = [i for i in range(10)] it2 = [(1, -1), (2, -1), (3, -4), (5, 6)] it3 = (1, 2, 3, 4, 5) t = async_iter("math.floor", it, sync=True) t2 = async_iter("math.copysign", it2, sync=True) t3 = async_iter("math.floor", it3, sync=True) t4 = async_iter("math.floor", (1,), sync=True) result_t = result(t) assert result_t is not None task_t = fetch(t) assert task_t.result == result_t assert result(t2) is not None assert result(t3) is not None assert result(t4)[0] == 1 # test iter class i = Iter("math.copysign", sync=True, cached=True) i.append(1, -1) i.append(2, -1) i.append(3, -4) i.append(5, 6) assert i.started is False assert i.length() == 4 assert i.run() is not None assert len(i.result()) == 4 assert len(i.fetch().result) == 4 i.append(1, -7) assert i.result() is None i.run() assert len(i.result()) == 5 @pytest.mark.django_db def test_chain(broker): broker.purge_queue() broker.cache.clear() task_chain = Chain(sync=True) task_chain.append("math.floor", 1) task_chain.append("math.copysign", 1, -1) task_chain.append("math.floor", 2) assert task_chain.length() == 3 assert task_chain.current() is None task_chain.run() r = task_chain.result(wait=1000) assert task_chain.current() == task_chain.length() assert len(r) == task_chain.length() t = task_chain.fetch() assert len(t) == task_chain.length() task_chain.cached = True task_chain.append("math.floor", 3) assert task_chain.length() == 4 task_chain.run() r = task_chain.result(wait=1000) assert task_chain.current() == task_chain.length() assert len(r) == task_chain.length() t = task_chain.fetch() assert len(t) == task_chain.length() # test single rid = async_chain( ["django_q.tests.tasks.hello", "django_q.tests.tasks.hello"], sync=True, cached=True, ) assert result_group(rid, cached=True) == ["hello", "hello"] @pytest.mark.django_db def test_asynctask_class(broker, monkeypatch): broker.purge_queue() broker.cache.clear() a = AsyncTask("math.copysign") assert a.func == "math.copysign" a.args = (1, -1) assert a.started is False a.cached = True assert a.cached a.sync = True assert a.sync a.broker = broker assert a.broker == broker a.run() assert a.result() == -1 assert a.fetch().result == -1 # again with kwargs a = AsyncTask("math.copysign", 1, -1, cached=True, sync=True, broker=broker) a.run() assert a.result() == -1 # with q_options a = AsyncTask( "math.copysign", 1, -1, q_options={"cached": True, "sync": False, "broker": broker}, ) assert not a.sync a.sync = True assert a.kwargs["q_options"]["sync"] is True a.run() assert a.result() == -1 a.group = "async_class_test" assert a.group == "async_class_test" a.save = False assert not a.save a.hook = "djq.tests.tasks.hello" assert a.hook == "djq.tests.tasks.hello" assert a.started is False a.run() assert a.result_group() == [-1] assert a.fetch_group() == [a.fetch()] # global overrides monkeypatch.setattr(Conf, "SYNC", True) monkeypatch.setattr(Conf, "CACHED", True) a = AsyncTask("math.floor", 1.5) a.run() assert a.result() == 1 django-q2-1.7.4/django_q/tests/test_cluster.py000066400000000000000000000606251471170400300213310ustar00rootroot00000000000000import os import sys import threading import uuid as uuidlib from datetime import datetime from math import copysign from multiprocessing import Event, Value from time import sleep from typing import Optional import pytest from django.utils import timezone from django_q.brokers import Broker, get_broker from django_q.cluster import Cluster, Sentinel from django_q.conf import Conf from django_q.humanhash import DEFAULT_WORDLIST, uuid from django_q.models import Success, Task from django_q.monitor import monitor, save_task from django_q.pusher import pusher from django_q.queues import Queue from django_q.signals import post_execute, pre_enqueue, pre_execute from django_q.status import Stat from django_q.tasks import ( async_task, count_group, delete_group, fetch, fetch_group, queue_size, result, result_group, ) from django_q.tests.tasks import TaskError, multiply from django_q.utils import add_months, add_years from django_q.worker import worker myPath = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, myPath + "/../") class WordClass: def __init__(self): self.word_list = DEFAULT_WORDLIST def get_words(self): return self.word_list @pytest.fixture def broker(monkeypatch): monkeypatch.setattr(Conf, "DJANGO_REDIS", "default") return get_broker() def test_redis_connection(broker): assert broker.ping() is True @pytest.mark.django_db def test_sync(broker): task = async_task( "django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker, sync=True ) assert result(task) == 1506 @pytest.mark.django_db def test_sync_raise_exception(broker): with pytest.raises(TaskError): async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True) @pytest.mark.django_db def test_cluster_initial(broker): broker.list_key = "initial_test:q" broker.delete_queue() c = Cluster(broker=broker) assert c.sentinel is None assert c.stat.status == Conf.STOPPED assert c.start() > 0 assert c.sentinel.is_alive() is True assert c.is_running assert c.is_stopping is False assert c.is_starting is False sleep(0.5) stat = c.stat assert stat.status == Conf.IDLE assert c.stop() is True assert c.sentinel.is_alive() is False assert c.has_stopped assert c.stop() is False broker.delete_queue() @pytest.mark.django_db def test_sentinel(): start_event = Event() stop_event = Event() stop_event.set() cluster_id = uuidlib.uuid4() s = Sentinel( stop_event, start_event, cluster_id=cluster_id, broker=get_broker("sentinel_test:q"), ) assert start_event.is_set() assert s.status() == Conf.STOPPED @pytest.mark.django_db def test_cluster(broker): broker.list_key = "cluster_test:q" broker.delete_queue() task = async_task( "django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker ) assert broker.queue_size() == 1 task_queue = Queue() assert task_queue.qsize() == 0 result_queue = Queue() assert result_queue.qsize() == 0 event = Event() event.set() # Test push pusher(task_queue, event, broker=broker) assert task_queue.qsize() == 1 assert queue_size(broker=broker) == 0 # Test work task_queue.put("STOP") worker(task_queue, result_queue, Value("f", -1)) assert task_queue.qsize() == 0 assert result_queue.qsize() == 1 # Test monitor result_queue.put("STOP") monitor(result_queue) assert result_queue.qsize() == 0 # check result assert result(task) == 1506 broker.delete_queue() @pytest.mark.django_db def test_results(broker): broker.list_key = "cluster_test:q" broker.delete_queue() a = async_task( "django_q.tests.tasks.return_falsy_value", broker=broker, ) task_queue = Queue() stop_event = Event() stop_event.set() pusher(task_queue, stop_event, broker=broker) task_queue.put("STOP") result_queue = Queue() worker(task_queue, result_queue, Value("f", -1)) result_queue.put("STOP") monitor(result_queue) # should not loop indefinitely when a real value is returned value = result(a, wait=-1) assert value == [] @pytest.mark.django_db def test_enqueue(broker, admin_user): broker.list_key = "cluster_test:q" broker.delete_queue() a = async_task( "django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, hook="django_q.tests.test_cluster.assert_result", broker=broker, ) b = async_task( "django_q.tests.tasks.count_letters2", WordClass(), hook="django_q.tests.test_cluster.assert_result", broker=broker, ) # unknown argument c = async_task( "django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, "oneargumentoomany", hook="django_q.tests.test_cluster.assert_bad_result", broker=broker, ) # unknown function d = async_task( "django_q.tests.tasks.does_not_exist", WordClass(), hook="django_q.tests.test_cluster.assert_bad_result", broker=broker, ) # function without result e = async_task("django_q.tests.tasks.countdown", 100000, broker=broker) # function as instance f = async_task(multiply, 753, 2, hook=assert_result, broker=broker) # model as argument g = async_task( "django_q.tests.tasks.get_task_name", Task(name="John"), broker=broker ) # args,kwargs, group and broken hook h = async_task( "django_q.tests.tasks.word_multiply", 2, word="django", hook="fail.me", broker=broker, ) # args unpickle test j = async_task( "django_q.tests.tasks.get_user_id", admin_user, broker=broker, group="test_j" ) # q_options and save opt_out test k = async_task( "django_q.tests.tasks.get_user_id", admin_user, q_options={"broker": broker, "group": "test_k", "save": False, "timeout": 90}, ) # test unicode assert Task(name="Amalia").__str__() == "Amalia" # check if everything has a task id assert isinstance(a, str) assert isinstance(b, str) assert isinstance(c, str) assert isinstance(d, str) assert isinstance(e, str) assert isinstance(f, str) assert isinstance(g, str) assert isinstance(h, str) assert isinstance(j, str) assert isinstance(k, str) # run the cluster to execute the tasks task_count = 10 assert broker.queue_size() == task_count task_queue = Queue() stop_event = Event() stop_event.set() # push the tasks for _ in range(task_count): pusher(task_queue, stop_event, broker=broker) assert broker.queue_size() == 0 assert task_queue.qsize() == task_count task_queue.put("STOP") # test wait timeout assert result(j, wait=10) is None assert fetch(j, wait=10) is None assert result_group("test_j", wait=10) is None assert result_group("test_j", count=2, wait=10) is None assert fetch_group("test_j", wait=10) is None assert fetch_group("test_j", count=2, wait=10) is None # let a worker handle them result_queue = Queue() worker(task_queue, result_queue, Value("f", -1)) assert result_queue.qsize() == task_count result_queue.put("STOP") # store the results monitor(result_queue) assert result_queue.qsize() == 0 # Check the results # task a result_a = fetch(a) assert result_a is not None assert result_a.success is True assert result(a) == 1506 # task b result_b = fetch(b) assert result_b is not None assert result_b.success is True assert result(b) == 1506 # task c result_c = fetch(c) assert result_c is not None assert result_c.success is False # task d result_d = fetch(d) assert result_d is not None assert result_d.success is False # task e result_e = fetch(e) assert result_e is not None assert result_e.success is True assert result(e) is None # task f result_f = fetch(f) assert result_f is not None assert result_f.success is True assert result(f) == 1506 # task g result_g = fetch(g) assert result_g is not None assert result_g.success is True assert result(g) == "John" # task h result_h = fetch(h) assert result_h is not None assert result_h.success is True assert result(h) == 12 # task j result_j = fetch(j) assert result_j is not None assert result_j.success is True assert result_j.result == result_j.args[0].id # check fetch, result by name assert fetch(result_j.name) == result_j assert result(result_j.name) == result_j.result # groups assert result_group("test_j")[0] == result_j.result assert result_j.group_result()[0] == result_j.result assert result_group("test_j", failures=True)[0] == result_j.result assert result_j.group_result(failures=True)[0] == result_j.result assert fetch_group("test_j")[0].id == [result_j][0].id assert fetch_group("test_j", failures=False)[0].id == [result_j][0].id assert count_group("test_j") == 1 assert result_j.group_count() == 1 assert count_group("test_j", failures=True) == 0 assert result_j.group_count(failures=True) == 0 assert delete_group("test_j") == 1 assert result_j.group_delete() == 0 deleted_group = delete_group("test_j", tasks=True) assert deleted_group is None or deleted_group[0] == 0 # Django 1.9 deleted_group = result_j.group_delete(tasks=True) assert deleted_group is None or deleted_group[0] == 0 # Django 1.9 # task k should not have been saved assert fetch(k) is None assert fetch(k, 100) is None assert result(k, 100) is None broker.delete_queue() @pytest.mark.django_db @pytest.mark.parametrize( "cluster_config_timeout, async_task_kwargs", ( (1, {}), (10, {"timeout": 1}), (None, {"timeout": 1}), ), ) def test_timeout(broker, cluster_config_timeout, async_task_kwargs): # set up the Sentinel broker.list_key = "timeout_test:q" broker.purge_queue() async_task("time.sleep", 5, broker=broker, **async_task_kwargs) start_event = Event() stop_event = Event() cluster_id = uuidlib.uuid4() # Set a timer to stop the Sentinel threading.Timer(3, stop_event.set).start() s = Sentinel( stop_event, start_event, cluster_id=cluster_id, broker=broker, timeout=cluster_config_timeout, ) assert start_event.is_set() assert s.status() == Conf.STOPPED assert s.reincarnations == 1 broker.delete_queue() @pytest.mark.django_db @pytest.mark.parametrize( "cluster_config_timeout, async_task_kwargs", ( (5, {}), (10, {"timeout": 5}), (1, {"timeout": 5}), (None, {"timeout": 5}), ), ) def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs): # set up the Sentinel broker.list_key = "timeout_test:q" broker.purge_queue() async_task("time.sleep", 3, broker=broker, **async_task_kwargs) start_event = Event() stop_event = Event() cluster_id = uuidlib.uuid4() # Set a timer to stop the Sentinel threading.Timer(6, stop_event.set).start() s = Sentinel( stop_event, start_event, cluster_id=cluster_id, broker=broker, timeout=cluster_config_timeout, ) assert start_event.is_set() assert s.status() == Conf.STOPPED assert s.reincarnations == 0 broker.delete_queue() @pytest.mark.django_db def test_recycle(broker, monkeypatch): # set up the Sentinel broker.list_key = "test_recycle_test:q" async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) start_event = Event() stop_event = Event() cluster_id = uuidlib.uuid4() # override settings monkeypatch.setattr(Conf, "RECYCLE", 2) monkeypatch.setattr(Conf, "WORKERS", 1) # set a timer to stop the Sentinel threading.Timer(3, stop_event.set).start() s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) assert start_event.is_set() assert s.status() == Conf.STOPPED assert s.reincarnations == 1 async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) task_queue = Queue() result_queue = Queue() # push two tasks pusher(task_queue, stop_event, broker=broker) pusher(task_queue, stop_event, broker=broker) # worker should exit on recycle worker(task_queue, result_queue, Value("f", -1)) # check if the work has been done assert result_queue.qsize() == 2 # save_limit test monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) result_queue.put("STOP") # run monitor monitor(result_queue) assert Success.objects.count() == Conf.SAVE_LIMIT broker.delete_queue() @pytest.mark.django_db def test_save_limit_per_func(broker, monkeypatch): # set up the Sentinel broker.list_key = "test_recycle_test:q" async_task("django_q.tests.tasks.hello", broker=broker) async_task("django_q.tests.tasks.countdown", 2, broker=broker) async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) start_event = Event() stop_event = Event() cluster_id = uuidlib.uuid4() task_queue = Queue() result_queue = Queue() # override settings monkeypatch.setattr(Conf, "RECYCLE", 3) monkeypatch.setattr(Conf, "WORKERS", 1) # set a timer to stop the Sentinel threading.Timer(3, stop_event.set).start() for i in range(3): pusher(task_queue, stop_event, broker=broker) worker(task_queue, result_queue, Value("f", -1)) s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) assert start_event.is_set() assert s.status() == Conf.STOPPED # worker should exit on recycle # check if the work has been done assert result_queue.qsize() == 3 # save_limit test monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func") result_queue.put("STOP") # run monitor monitor(result_queue) assert Success.objects.count() == 3 assert set(Success.objects.filter().values_list("func", flat=True)) == { "django_q.tests.tasks.countdown", "django_q.tests.tasks.hello", "django_q.tests.tasks.multiply", } broker.delete_queue() @pytest.mark.django_db def test_max_rss(broker, monkeypatch): # set up the Sentinel broker.list_key = "test_max_rss_test:q" async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) start_event = Event() stop_event = Event() cluster_id = uuidlib.uuid4() # override settings monkeypatch.setattr(Conf, "MAX_RSS", 20000) monkeypatch.setattr(Conf, "WORKERS", 1) # set a timer to stop the Sentinel threading.Timer(3, stop_event.set).start() s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker) assert start_event.is_set() assert s.status() == Conf.STOPPED assert s.reincarnations == 1 async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) task_queue = Queue() result_queue = Queue() # push the task pusher(task_queue, stop_event, broker=broker) # worker should exit on recycle worker(task_queue, result_queue, Value("f", -1)) # check if the work has been done assert result_queue.qsize() == 1 # save_limit test monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) result_queue.put("STOP") # run monitor monitor(result_queue) assert Success.objects.count() == Conf.SAVE_LIMIT broker.delete_queue() @pytest.mark.django_db def test_bad_secret(broker, monkeypatch): broker.list_key = "test_bad_secret:q" async_task("math.copysign", 1, -1, broker=broker) stop_event = Event() stop_event.set() start_event = Event() cluster_id = uuidlib.uuid4() s = Sentinel( stop_event, start_event, cluster_id=cluster_id, broker=broker, start=False ) Stat(s).save() # change the SECRET monkeypatch.setattr(Conf, "SECRET_KEY", "OOPS") stat = Stat.get_all() assert len(stat) == 0 assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None task_queue = Queue() pusher(task_queue, stop_event, broker=broker) result_queue = Queue() task_queue.put("STOP") worker( task_queue, result_queue, Value("f", -1), ) assert result_queue.qsize() == 0 broker.delete_queue() @pytest.mark.django_db def test_attempt_count(broker, monkeypatch): monkeypatch.setattr(Conf, "MAX_ATTEMPTS", 3) tag = uuid() task = { "id": tag[1], "name": tag[0], "func": "math.copysign", "args": (1, -1), "kwargs": {}, "started": timezone.now(), "stopped": timezone.now(), "success": False, "result": None, } # initial save - no success save_task(task, broker) assert Task.objects.filter(id=task["id"]).exists() saved_task = Task.objects.get(id=task["id"]) assert saved_task.attempt_count == 1 sleep(0.5) # second save task["stopped"] = timezone.now() save_task(task, broker) saved_task = Task.objects.get(id=task["id"]) assert saved_task.attempt_count == 2 # third save - task["stopped"] = timezone.now() save_task(task, broker) saved_task = Task.objects.get(id=task["id"]) assert saved_task.attempt_count == 3 # task should be removed from queue assert broker.queue_size() == 0 @pytest.mark.django_db def test_update_failed(broker): tag = uuid() task = { "id": tag[1], "name": tag[0], "func": "math.copysign", "args": (1, -1), "kwargs": {}, "started": timezone.now(), "stopped": timezone.now(), "success": False, "result": None, } # initial save - no success save_task(task, broker) assert Task.objects.filter(id=task["id"]).exists() saved_task = Task.objects.get(id=task["id"]) assert saved_task.success is False sleep(0.5) # second save - no success old_stopped = task["stopped"] task["stopped"] = timezone.now() save_task(task, broker) saved_task = Task.objects.get(id=task["id"]) assert saved_task.stopped > old_stopped # third save - success task["stopped"] = timezone.now() task["result"] = "result" task["success"] = True save_task(task, broker) saved_task = Task.objects.get(id=task["id"]) assert saved_task.success is True # fourth save - no success task["result"] = None task["success"] = False task["stopped"] = old_stopped save_task(task, broker) # should not overwrite success saved_task = Task.objects.get(id=task["id"]) assert saved_task.success is True assert saved_task.result == "result" @pytest.mark.django_db def test_acknowledge_failure_override(): class VerifyAckMockBroker(Broker): def __init__(self, *args, **kwargs): super(VerifyAckMockBroker, self).__init__(*args, **kwargs) self.acknowledgements = {} def acknowledge(self, task_id): count = self.acknowledgements.get(task_id, 0) self.acknowledgements[task_id] = count + 1 tag = uuid() task_fail_ack = { "id": tag[1], "name": tag[0], "ack_id": "test_fail_ack_id", "ack_failure": True, "func": "math.copysign", "args": (1, -1), "kwargs": {}, "started": timezone.now(), "stopped": timezone.now(), "success": False, "result": None, } tag = uuid() task_fail_no_ack = task_fail_ack.copy() task_fail_no_ack.update( {"id": tag[1], "name": tag[0], "ack_id": "test_fail_no_ack_id"} ) del task_fail_no_ack["ack_failure"] tag = uuid() task_success_ack = task_fail_ack.copy() task_success_ack.update( { "id": tag[1], "name": tag[0], "ack_id": "test_success_ack_id", "success": True, } ) del task_success_ack["ack_failure"] result_queue = Queue() result_queue.put(task_fail_ack) result_queue.put(task_fail_no_ack) result_queue.put(task_success_ack) result_queue.put("STOP") broker = VerifyAckMockBroker(list_key="key") monitor(result_queue, broker) assert broker.acknowledgements.get("test_fail_ack_id") == 1 assert broker.acknowledgements.get("test_fail_no_ack_id") is None assert broker.acknowledgements.get("test_success_ack_id") == 1 class TestSignals: @pytest.mark.django_db def test_pre_enqueue_signal(self, broker): broker.list_key = "pre_enqueue_test:q" broker.delete_queue() self.signal_was_called: bool = False self.task: Optional[dict] = None def handler(sender, task, **kwargs): self.signal_was_called = True self.task = task pre_enqueue.connect(handler) task_id = async_task("math.copysign", 1, -1, broker=broker) assert self.signal_was_called is True assert self.task.get("id") == task_id pre_enqueue.disconnect(handler) broker.delete_queue() @pytest.mark.django_db def test_pre_execute_signal(self, broker): broker.list_key = "pre_execute_test:q" broker.delete_queue() self.signal_was_called: bool = False self.task: Optional[dict] = None self.func = None def handler(sender, task, func, **kwargs): self.signal_was_called = True self.task = task self.func = func pre_execute.connect(handler) task_id = async_task("math.copysign", 1, -1, broker=broker) task_queue = Queue() result_queue = Queue() event = Event() event.set() pusher(task_queue, event, broker=broker) task_queue.put("STOP") worker(task_queue, result_queue, Value("f", -1)) result_queue.put("STOP") monitor(result_queue, broker) broker.delete_queue() assert self.signal_was_called is True assert self.task.get("id") == task_id assert self.func == copysign pre_execute.disconnect(handler) @pytest.mark.django_db def test_post_execute_signal(self, broker): broker.list_key = "post_execute_test:q" broker.delete_queue() self.signal_was_called: bool = False self.task: Optional[dict] = None self.func = None def handler(sender, task, **kwargs): self.signal_was_called = True self.task = task post_execute.connect(handler) task_id = async_task("math.copysign", 1, -1, broker=broker) task_queue = Queue() result_queue = Queue() event = Event() event.set() pusher(task_queue, event, broker=broker) task_queue.put("STOP") worker(task_queue, result_queue, Value("f", -1)) result_queue.put("STOP") monitor(result_queue, broker) broker.delete_queue() assert self.signal_was_called is True assert self.task.get("id") == task_id assert self.task.get("result") == -1 post_execute.disconnect(handler) @pytest.mark.django_db def assert_result(task): assert task is not None assert task.success is True assert task.result == 1506 @pytest.mark.django_db def assert_bad_result(task): assert task is not None assert task.success is False @pytest.mark.django_db def test_add_months(): # add some months initial_date = datetime(2020, 2, 2) new_date = add_months(initial_date, 3) assert new_date.year == 2020 assert new_date.month == 5 assert new_date.day == 2 # push to next year initial_date = datetime(2020, 11, 2) new_date = add_months(initial_date, 3) assert new_date.year == 2021 assert new_date.month == 2 assert new_date.day == 2 # last day of the month initial_date = datetime(2020, 1, 31) new_date = add_months(initial_date, 1) assert new_date.year == 2020 assert new_date.month == 2 assert new_date.day == 29 @pytest.mark.django_db def test_add_years(): # add some months initial_date = datetime(2020, 2, 2) new_date = add_years(initial_date, 1) assert new_date.year == 2021 assert new_date.month == 2 assert new_date.day == 2 # test leap year initial_date = datetime(2020, 2, 29) new_date = add_years(initial_date, 1) assert new_date.year == 2021 assert new_date.month == 2 assert new_date.day == 28 django-q2-1.7.4/django_q/tests/test_commands.py000066400000000000000000000010231471170400300214340ustar00rootroot00000000000000import pytest from django.core.management import call_command @pytest.mark.django_db def test_qcluster(): call_command("qcluster", run_once=True) @pytest.mark.django_db def test_qmonitor(): call_command("qmonitor", run_once=True) @pytest.mark.django_db def test_qinfo(): call_command("qinfo") call_command("qinfo", config=True) call_command("qinfo", ids=True) @pytest.mark.django_db def test_qmemory(): call_command("qmemory", run_once=True) call_command("qmemory", workers=True, run_once=True) django-q2-1.7.4/django_q/tests/test_monitor.py000066400000000000000000000023301471170400300213240ustar00rootroot00000000000000import uuid import pytest from django_q.brokers import get_broker from django_q.cluster import Cluster from django_q.conf import Conf from django_q.monitor_terminal import get_ids, info, monitor from django_q.status import Stat from django_q.tasks import async_task @pytest.mark.django_db def test_monitor(monkeypatch): cluster_id = uuid.uuid4() assert Stat.get(pid=0, cluster_id=cluster_id).sentinel == 0 c = Cluster() c.start() stats = monitor(run_once=True) assert get_ids() is True c.stop() assert len(stats) > 0 found_c = False for stat in stats: if stat.cluster_id == c.cluster_id: found_c = True assert stat.uptime() > 0 assert stat.empty_queues() is True break assert found_c # test lock size monkeypatch.setattr(Conf, "ORM", "default") b = get_broker("monitor_test") b.enqueue("test") b.dequeue() assert b.lock_size() == 1 monitor(run_once=True, broker=b) b.delete_queue() @pytest.mark.django_db def test_info(): info() do_sync() info() for _ in range(24): do_sync() info() def do_sync(): async_task("django_q.tests.tasks.countdown", 1, sync=True, save=True) django-q2-1.7.4/django_q/tests/test_scheduler.py000066400000000000000000000351151471170400300216220ustar00rootroot00000000000000import os from datetime import datetime, timedelta from multiprocessing import Event, Value from unittest import mock import django import pytest from django.core.exceptions import ValidationError from django.db import IntegrityError from django.test import override_settings from django.utils import timezone from django.utils.timezone import is_naive from django_q.brokers import Broker, get_broker from django_q.conf import Conf from django_q.monitor import monitor from django_q.pusher import pusher from django_q.queues import Queue from django_q.scheduler import scheduler from django_q.tasks import Schedule, fetch from django_q.tasks import schedule as create_schedule from django_q.tests.settings import BASE_DIR from django_q.tests.testing_utilities.multiple_database_routers import ( TestingMultipleAppsDatabaseRouter, TestingReplicaDatabaseRouter, ) from django_q.utils import add_months, localtime from django_q.worker import worker if django.VERSION < (4, 0): # pytz is the default in django 3.2. Remove when no support for 3.2 from pytz import timezone as ZoneInfo else: try: from zoneinfo import ZoneInfo except ImportError: from backports.zoneinfo import ZoneInfo @pytest.fixture def broker(monkeypatch) -> Broker: """ Patches the Conf object setting the DJANGO_REDIS attribute allowing a default redis configuration. """ monkeypatch.setattr(Conf, "DJANGO_REDIS", "default") return get_broker() @pytest.fixture def orm_broker(monkeypatch) -> None: """Patches the Conf object setting the ORM attribute to a database named default.""" monkeypatch.setattr(Conf, "ORM", "default") REPLICA_DATABASE_ROUTERS = [ f"{TestingReplicaDatabaseRouter.__module__}.{TestingReplicaDatabaseRouter.__name__}" ] REPLICA_DATABASES = { "writable": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, "replica": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, } MULTIPLE_APPS_DATABASE_ROUTERS = [ f"{TestingMultipleAppsDatabaseRouter.__module__}.{TestingMultipleAppsDatabaseRouter.__name__}" # noqa: E501 ] MULTIPLE_APPS_DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, "admin": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, } @pytest.mark.django_db def test_scheduler_daylight_saving_time_daily(broker, monkeypatch): # Set up a startdate in the Amsterdam timezone (without dst 1 hour ahead). The # 28th of March 2021 is the day when sunlight saving starts (at 2 am) monkeypatch.setattr(Conf, "TIME_ZONE", "Europe/Amsterdam") tz = ZoneInfo("Europe/Amsterdam") broker.list_key = "scheduler_test:q" # Let's start a schedule at 1 am on the 27th of March. This is in AMS timezone. # So, 2021-03-27 00:00:00 when saved (due to TZ being Amsterdam and saved in UTC) start_date = datetime(2021, 3, 27, 1, 0, 0) # Create schedule with the next run date on the start date. It will move one day # forward when we run the scheduler schedule = create_schedule( "math.copysign", 1, -1, name="test math", schedule_type=Schedule.DAILY, next_run=start_date, ) # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() # It's now the day after exactly at midnight UTC next_run = schedule.next_run assert str(next_run) == "2021-03-28 00:00:00+00:00" # In the Amsterdam timezone, it's 1 hour over midnight (+01) next_run = next_run.astimezone(tz) assert str(next_run) == "2021-03-28 01:00:00+01:00" # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() next_run = schedule.next_run assert str(next_run) == "2021-03-28 23:00:00+00:00" next_run = next_run.astimezone(tz) # In the Amsterdam timezone, it's 1 hour over midnight (+02) assert str(next_run) == "2021-03-29 01:00:00+02:00" # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() next_run = schedule.next_run assert str(next_run) == "2021-03-29 23:00:00+00:00" next_run = next_run.astimezone(tz) assert str(next_run) == "2021-03-30 01:00:00+02:00" # Create second schedule with the next run date on the start date. It will move # one day forward when we run the scheduler start_date = datetime(2021, 10, 29, 1, 0, 0) schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, name="multiply", schedule_type=Schedule.DAILY, next_run=start_date, ) # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() next_run = schedule.next_run assert str(next_run) == "2021-10-29 23:00:00+00:00" # In the Amsterdam timezone, it's 1 hour over midnight (+02) next_run = next_run.astimezone(tz) assert str(next_run) == "2021-10-30 01:00:00+02:00" # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() next_run = schedule.next_run assert str(next_run) == "2021-10-30 23:00:00+00:00" # In the Amsterdam timezone, it's 1 hour over midnight (+02) next_run = next_run.astimezone(tz) assert str(next_run) == "2021-10-31 01:00:00+02:00" # Run scheduler so we get the next run date scheduler(broker=broker) schedule.refresh_from_db() next_run = schedule.next_run assert str(next_run) == "2021-11-01 00:00:00+00:00" # In the Amsterdam timezone, it's 1 hour over midnight (+01) # Switch of DST next_run = next_run.astimezone(tz) assert str(next_run) == "2021-11-01 01:00:00+01:00" @pytest.mark.django_db def test_scheduler(broker, monkeypatch): broker.list_key = "scheduler_test:q" broker.delete_queue() schedule = create_schedule( "math.copysign", 1, -1, name="test math", hook="django_q.tests.tasks.result", schedule_type=Schedule.HOURLY, repeats=1, ) assert schedule.last_run() is None # check duplicate constraint with pytest.raises(IntegrityError): schedule = create_schedule( "math.copysign", 1, -1, name="test math", hook="django_q.tests.tasks.result", schedule_type=Schedule.HOURLY, repeats=1, ) # run scheduler scheduler(broker=broker) # set up the workflow task_queue = Queue() stop_event = Event() stop_event.set() # push it pusher(task_queue, stop_event, broker=broker) assert task_queue.qsize() == 1 assert broker.queue_size() == 0 task_queue.put("STOP") # let a worker handle them result_queue = Queue() worker(task_queue, result_queue, Value("b", -1)) assert result_queue.qsize() == 1 result_queue.put("STOP") # store the results monitor(result_queue) assert result_queue.qsize() == 0 schedule = Schedule.objects.get(pk=schedule.pk) assert schedule.repeats == 0 assert schedule.last_run() is not None assert schedule.success() is True assert schedule.next_run < timezone.now() + timedelta(hours=1) task = fetch(schedule.task) assert task is not None assert task.success is True assert task.result < 0 # Once schedule with delete once_schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=Schedule.ONCE, repeats=-1, hook="django_q.tests.tasks.result", ) assert hasattr(once_schedule, "pk") is True # negative repeats always_schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=Schedule.DAILY, repeats=-1, hook="django_q.tests.tasks.result", ) assert hasattr(always_schedule, "pk") is True # Minute schedule minute_schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=Schedule.MINUTES, minutes=10, ) assert hasattr(minute_schedule, "pk") is True # Cron schedule cron_schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=Schedule.CRON, cron="0 22 * * 1-5", ) assert hasattr(cron_schedule, "pk") is True assert cron_schedule.full_clean() is None assert cron_schedule.__str__() == "django_q.tests.tasks.word_multiply" with pytest.raises(ValidationError): create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=Schedule.CRON, cron="0 22 * * 1-12", ) # All other types for t in Schedule.TYPE: if t[0] == Schedule.CRON: continue schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="django", schedule_type=t[0], repeats=1, hook="django_q.tests.tasks.result", ) assert schedule is not None assert schedule.last_run() is None scheduler(broker=broker) # via model Schedule.objects.create( func="django_q.tests.tasks.word_multiply", args="2", kwargs='word="django"', schedule_type=Schedule.DAILY, ) # scheduler scheduler(broker=broker) # ONCE schedule should be deleted assert Schedule.objects.filter(pk=once_schedule.pk).exists() is False # Catch up On monkeypatch.setattr(Conf, "CATCH_UP", True) now = timezone.now() schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="catch_up", schedule_type=Schedule.HOURLY, next_run=timezone.now() - timedelta(hours=12), repeats=-1, ) scheduler(broker=broker) schedule = Schedule.objects.get(pk=schedule.pk) assert schedule.next_run < now # Catch up off monkeypatch.setattr(Conf, "CATCH_UP", False) scheduler(broker=broker) schedule = Schedule.objects.get(pk=schedule.pk) assert schedule.next_run > now # Done broker.delete_queue() # test bimonthly schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="catch_up", schedule_type=Schedule.BIMONTHLY, ) scheduler(broker=broker) schedule = Schedule.objects.get(pk=schedule.pk) assert schedule.next_run.date() == add_months(timezone.now(), 2).date() # test biweekly schedule = create_schedule( "django_q.tests.tasks.word_multiply", 2, word="catch_up", schedule_type=Schedule.BIWEEKLY, ) scheduler(broker=broker) schedule = Schedule.objects.get(pk=schedule.pk) assert schedule.next_run.date() == (timezone.now() + timedelta(weeks=2)).date() broker.delete_queue() monkeypatch.setattr(Conf, "CLUSTER_NAME", "some_cluster_name") # create a schedule on another cluster schedule = create_schedule( "math.copysign", 1, -1, name="test schedule on a another cluster", hook="django_q.tests.tasks.result", schedule_type=Schedule.HOURLY, cluster="some_other_cluster_name", repeats=1, ) # run scheduler scheduler(broker=broker) # set up the workflow task_queue = Queue() stop_event = Event() stop_event.set() # push it pusher(task_queue, stop_event, broker=broker) # queue must be empty assert task_queue.qsize() == 0 monkeypatch.setattr(Conf, "CLUSTER_NAME", "default") # create a schedule on the same cluster schedule = create_schedule( "math.copysign", 1, -1, name="test schedule with no cluster", hook="django_q.tests.tasks.result", schedule_type=Schedule.HOURLY, cluster="default", repeats=1, ) # run scheduler scheduler(broker=broker) # set up the workflow task_queue = Queue() stop_event = Event() stop_event.set() # push it pusher(task_queue, stop_event, broker=broker) # queue must contain a task assert task_queue.qsize() == 1 @pytest.mark.django_db def test_intended_schedule_kwarg(broker, monkeypatch): broker.list_key = "scheduler_test:q" broker.delete_queue() run_date = timezone.now() - timedelta(hours=1) schedule = create_schedule( "math.copysign", 1, -1, name="test math", hook="django_q.tests.tasks.result", schedule_type=Schedule.HOURLY, repeats=1, next_run=run_date, intended_date_kwarg="intended_date", ) assert schedule.last_run() is None assert schedule.intended_date_kwarg == "intended_date" # run scheduler scheduler(broker=broker) # set up the workflow task_queue = Queue() stop_event = Event() stop_event.set() # push it pusher(task_queue, stop_event, broker=broker) assert task_queue.qsize() == 1 task = task_queue.get() assert "intended_date" in task["kwargs"] assert task["kwargs"]["intended_date"] == run_date.isoformat() @override_settings( DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES ) @pytest.mark.django_db def test_scheduler_atomic_must_specify_the_write_db( orm_broker: Broker, ): """ GIVEN a environment with a read/write configured replica database WHEN the scheduler is called THEN the transaction must be called with the write database. """ broker = get_broker(list_key="scheduler_test:q") with mock.patch("django_q.cluster.db.transaction") as mocked_db: scheduler(broker=broker) mocked_db.atomic.assert_called_with(using="writable") @override_settings( DATABASE_ROUTERS=MULTIPLE_APPS_DATABASE_ROUTERS, DATABASES=MULTIPLE_APPS_DATABASES ) @pytest.mark.django_db def test_scheduler_atomic_must_specify_the_database_based_on_router_redirection( orm_broker: Broker, ): """ GIVEN a environment without a read replica database WHEN the scheduler is called THEN the transaction atomic must be called using the default connection. """ broker = get_broker(list_key="scheduler_test:q") with mock.patch("django_q.cluster.db.transaction") as mocked_db: scheduler(broker=broker) mocked_db.atomic.assert_called_with(using="default") def test_localtime(): assert not is_naive(localtime()) @override_settings(USE_TZ=False) def test_naive_localtime(): assert is_naive(localtime()) django-q2-1.7.4/django_q/tests/testing_utilities/000077500000000000000000000000001471170400300217765ustar00rootroot00000000000000django-q2-1.7.4/django_q/tests/testing_utilities/__init__.py000066400000000000000000000000001471170400300240750ustar00rootroot00000000000000django-q2-1.7.4/django_q/tests/testing_utilities/multiple_database_routers.py000066400000000000000000000016261471170400300276170ustar00rootroot00000000000000class TestingReplicaDatabaseRouter: """ A router to control all database operations on models in the auth application. """ def db_for_read(self, model, **hints): """ Allows read access from REPLICA database. """ return "replica" def db_for_write(self, model, **hints): """ Always write to WRITABLE database """ return "writable" class TestingMultipleAppsDatabaseRouter: """ A router to control all database operations on models in the auth application. """ @staticmethod def is_admin(model): return model._meta.app_label in ["admin"] def db_for_read(self, model, **hints): if self.is_admin(model): return "admin" return "default" def db_for_write(self, model, **hints): if self.is_admin(model): return "admin" return "default" django-q2-1.7.4/django_q/tests/urls.py000066400000000000000000000001761471170400300175710ustar00rootroot00000000000000from django.contrib import admin from django.urls import re_path urlpatterns = [ re_path(r"^admin/", admin.site.urls), ] django-q2-1.7.4/django_q/timeout.py000066400000000000000000000027051471170400300171300ustar00rootroot00000000000000import signal from django.utils.translation import gettext_lazy as _ from django_q.conf import logger from .exceptions import TimeoutException class TimeoutHandler: def __init__(self, timeout: int): self._timeout = timeout def raise_timeout_exception(self, signum, frame): raise TimeoutException( f"Task exceeded maximum timeout value ({self._timeout} seconds)" ) def __enter__(self): # if the timeout is -1, then there is no timeout and the task will always keep running until it's done or manually killed if self._timeout == -1: return try: signal.signal(signal.SIGALRM, self.raise_timeout_exception) signal.alarm(self._timeout) except ( ValueError, AttributeError, ): # AttributeError or ValueError might be raised for Windows users logger.debug(_("SIGALARM is not available on your platform")) def __exit__(self, exc_type, exc_value, traceback): if self._timeout == -1: return """When getting out of the timeout, reset the alarm, so it won't trigger""" try: signal.alarm(0) signal.signal(signal.SIGALRM, signal.SIG_DFL) except ( ValueError, AttributeError, ): # AttributeError or ValueError might be raised for Windows users logger.debug(_("SIGALARM is not available on your platform")) django-q2-1.7.4/django_q/utils.py000066400000000000000000000054101471170400300165760ustar00rootroot00000000000000import calendar import inspect from datetime import date, datetime import django from django import db from django.conf import settings from django.utils import timezone from django_q.conf import Conf, logger if django.VERSION < (4, 0): # pytz is the default in django 3.2. Remove when no support for 3.2 from pytz import timezone as ZoneInfo else: try: from zoneinfo import ZoneInfo except ImportError: from backports.zoneinfo import ZoneInfo # credits: https://stackoverflow.com/a/4131114 # Made them aware of timezone def add_months(d, months): month = d.month - 1 + months year = d.year + month // 12 month = month % 12 + 1 day = min(d.day, calendar.monthrange(year, month)[1]) return d.replace(year=year, month=month, day=day) # credits: https://stackoverflow.com/a/15743908 # Changed the last line to make it a little easier to read and changed it to move # February 29 to 28 next year. def add_years(d, years): """Return a date that's `years` years after the date (or datetime) object `d`. Return the same calendar date (month and day) in the destination year, if it exists, otherwise use the previous day (thus changing February 29 to February 28). """ try: return d.replace(year=d.year + years) except ValueError: new_date = d + (date(d.year + years, 3, 1) - date(d.year, 3, 1)) return d.replace(year=new_date.year, month=new_date.month, day=new_date.day) def get_func_repr(func): # convert func to string if inspect.isfunction(func): return f"{func.__module__}.{func.__name__}" elif inspect.ismethod(func) and hasattr(func.__self__, "__name__"): return ( f"{func.__self__.__module__}." f"{func.__self__.__name__}.{func.__name__}" ) else: return str(func) if func else None def localtime(value=None) -> datetime: """Override for timezone.localtime to deal with naive times and local times""" if settings.USE_TZ: if django.VERSION >= (4, 0) and getattr(settings, "USE_DEPRECATED_PYTZ", False): import pytz convert_to_tz = pytz.timezone(Conf.TIME_ZONE) else: convert_to_tz = ZoneInfo(Conf.TIME_ZONE) return timezone.localtime(value=value, timezone=convert_to_tz) if value is None: return datetime.now() else: return value def close_old_django_connections(): """ Close django connections unless running with sync=True. """ if Conf.SYNC: logger.warning( "Preserving django database connections because sync=True. Beware " "that tasks are now injected in the calling context/transactions " "which may result in unexpected behaviour." ) else: db.close_old_connections() django-q2-1.7.4/django_q/worker.py000066400000000000000000000113561471170400300167550ustar00rootroot00000000000000import pydoc import traceback from multiprocessing import Value from multiprocessing.process import current_process from multiprocessing.queues import Queue from django import core from django.apps.registry import apps from django.utils import timezone from django.utils.translation import gettext_lazy as _ try: apps.check_apps_ready() except core.exceptions.AppRegistryNotReady: import django django.setup() from django_q.conf import Conf, error_reporter, logger, resource, setproctitle from django_q.exceptions import TimeoutException from django_q.signals import post_spawn, pre_execute from django_q.timeout import TimeoutHandler from django_q.utils import close_old_django_connections, get_func_repr try: import psutil except ImportError: psutil = None try: import setproctitle except ModuleNotFoundError: setproctitle = None def worker( task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT ): """ Takes a task from the task queue, tries to execute it and puts the result back in the result queue :param timeout: number of seconds wait for a worker to finish. :type task_queue: multiprocessing.Queue :type result_queue: multiprocessing.Queue :type timer: multiprocessing.Value """ proc_name = current_process().name logger.info( _("%(proc_name)s ready for work at %(id)s") % {"proc_name": proc_name, "id": current_process().pid} ) post_spawn.send(sender="django_q", proc_name=proc_name) if setproctitle: setproctitle.setproctitle(f"qcluster {proc_name} idle") task_count = 0 if timeout is None: timeout = -1 # Start reading the task queue for task in iter(task_queue.get, "STOP"): result = None timer.value = -1 # Idle task_count += 1 f = task["func"] # Log task creation and set process name # Get the function from the task func_name = get_func_repr(f) task_name = task["name"] task_desc = _("%(proc_name)s processing %(task_name)s '%(func_name)s'") % { "proc_name": proc_name, "func_name": func_name, "task_name": task_name, } if "group" in task: task_desc += f" [{task['group']}]" logger.info(task_desc) if setproctitle: proc_title = f"qcluster {proc_name} processing {task_name} '{func_name}'" if "group" in task: proc_title += f" [{task['group']}]" setproctitle.setproctitle(proc_title) # if it's not an instance try to get it from the string if not callable(f): # locate() returns None if f cannot be loaded f = pydoc.locate(f) close_old_django_connections() timer_value = task.pop("timeout", timeout) # signal execution pre_execute.send(sender="django_q", func=f, task=task) # execute the payload timer.value = timer_value # Busy if timer.value != -1: timer.value += 3 # Add buffer so that guard doesn't kill the process on timeout before it gets processed timeout_error = False try: if f is None: # raise a meaningfull error if task["func"] is not a valid function raise ValueError(f"Function {task['func']} is not defined") with TimeoutHandler(timer_value): res = f(*task["args"], **task["kwargs"]) result = (res, True) except (Exception, TimeoutException) as e: if isinstance(e, TimeoutException): timeout_error = True result = (f"{e} : {traceback.format_exc()}", False) if error_reporter: error_reporter.report() if task.get("sync", False): raise with timer.get_lock(): # Process result task["result"] = result[0] task["success"] = result[1] task["stopped"] = timezone.now() result_queue.put(task) if timeout_error: # force destroy process due to timeout timer.value = 0 break timer.value = -1 # Idle if setproctitle: setproctitle.setproctitle(f"qcluster {proc_name} idle") # Recycle if task_count == Conf.RECYCLE or rss_check(): timer.value = -2 # Recycled break logger.info(_("%(proc_name)s stopped doing work") % {"proc_name": proc_name}) def rss_check(): if Conf.MAX_RSS: if resource: return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss >= Conf.MAX_RSS elif psutil: return psutil.Process().memory_info().rss >= Conf.MAX_RSS * 1024 return False django-q2-1.7.4/docs/000077500000000000000000000000001471170400300142325ustar00rootroot00000000000000django-q2-1.7.4/docs/Dockerfile000066400000000000000000000001641471170400300162250ustar00rootroot00000000000000FROM sphinxdoc/sphinx RUN mkdir -p /docs WORKDIR /docs COPY . . RUN pip3 install -r requirements.txt RUN make html django-q2-1.7.4/docs/Makefile000066400000000000000000000163711471170400300157020ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -nW SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoQ.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoQ.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoQ" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoQ" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." django-q2-1.7.4/docs/_static/000077500000000000000000000000001471170400300156605ustar00rootroot00000000000000django-q2-1.7.4/docs/_static/cluster.png000066400000000000000000001732431471170400300200610ustar00rootroot00000000000000‰PNG  IHDR˛QšŠTŦ IDATx^ėtĮÆ?$8$¸k)îÅŨĄZJ)E‹(VŦ8ü)^ xąEŠwwww ˆ@ō?wķ’&!!o_öŊˇģīÛsr ÍĖ{w¸ũ2ov&ø    € D3 Īt™H€H€H€H€@!ËI@$@$@$@$`H˛†L&    å    0$ YCĻN“    PČr    ’…Ŧ!ĶF§I€H€H€H€(d9H€H€H€H€ I€B֐iŖĶ$@$@$@$@˛œ$@ē"p˙ÕģŌrčĘ):ŖDۓ6QœKš¤! —&@!ëŌégđ$ ?÷_ž™hŅZëĪ3z¤ hŅÚĻIwļ&ļh„HĀå PČēü Đ Y}åCso(d5GJƒ$āĘ(d]9ûŒtH€BV‡IŅŌ% Y-iŌ ¸< Y—Ÿ@ú"@!̝|hî …ŦæHi\™…Ŧ+gŸą“€ PČę0)ZēD!Ģ%MÚ"—'@!ëōS€H@_(dõ•ÍŊĄÕ) ’€+ uåė3vĐ! Y&EK—(dĩ¤I[$āō(d]~  苅ŦžōĄš7˛š#ĨApe˛Žœ}ÆN:$@!Ģähé…Ŧ–4i‹\ž…ŦkMÔŅcÆ8Í-FL× Ûuĸ đ¸ëīã—ĪČSČ9{Vøn!ËziE:ÜÄ õŌČüĩōBV+’ư“>fü8+ĪĪîŌK5Ū=ķ ØÛķˇŪų&UĶOom?%d;ˇm…U˗~ärôčŅ'n\$Kžyķ@Ŗo›ĸjšá†ÖļYŦ_ŗCFŽAێõžSüyųâ/˜‡­›6āō… xũú&L„l9sĸJõšhŌŦ<'ÖÆ7ãYÖKm2ŽK+fŠ—ē„ë`§(d ÜÉÃĨwK÷Bí5ãâ;ŲooŸŧ øˇÅ—˛ááŦQ§.ĻΞ‡8qâ„ú1…lhZ+–.ƀŪ=ņĘĶ3ÂYéîáÉ3˙@åj5ĸ>s $dY/ŖžnŊZ0KŊÔ+_GúE!ëHÚ΋BÖų9°›f)ĖÖŦČÖoø5FŽŸĖ2 oŪŧÁÍë× Âl؟ •ŸÕũ˛ĻĪ]€hŅū+uk˙^‰+/ĸ|å*(\´˜ŨōaÃŗĻMÁ/?÷V\͕'/:víŽReĘ!q’$xx˙>öīŲ…é“ÃÕ˗ ĢŪ#'ü†ī[ļŽZh˛QãĮۚ0KŊÔ†ÁPČ<*Ũ§U ĖHÍÍR˜­˛_7iЉĶgF˜žsfŖo÷•Ÿ˙ąh)du–Oh'ŽEŨ*đáÃ|×ĸFMø 1bÄø“ˇˇ7ēūĐëV¯BėØąąy÷~dˑĶvœ˛ļŗcO͘Ĩ^jĆ(d œ<\§ĩšQē˜Ĩ0k!d%g-›|Íë˙Aū‚…°qį^Ŗ¤Ņa~6¨U öîAŠ2eą|íeÅ5ĸĮĪĪU˔ÄĨ įQ˛tŦ\ŋŲv?)dmgĮžš0KŊÔ ˆ QČ8y6¸N!k4Ŗt1KaÖJȊHą&ΉKא2UjåīŸÚ#{îĖiĖ™9‡öíÅÇđáũ{$Mž%J•Ɲē o‚ĄĻCîĖéņĘķ%î< Ų˛0{Ú\8NŲʐ¯@A´ëôc¸/ųûûã¯%báœŲ¸zå˛ŌžxÉRčÕo öė܁aûĄgßūčųs˙Pã={ú3&O–ëqįÖMÄtsCļė9Đ q4mŅ nnnVMWŗl‘JÛ՛ūEą’Ĩ"í'/‚5˙ĻĄŌn÷‘Ț-{¤}Âm@!k7öŌ”€YęĨĻP jŒBÖ ‰ŗŅm YÁĄ›Y ŗVBöũû÷Ț:|}}•}˛õž a YŲWÛŗsˆČôđHŒ4éŌ)oīßŋ{Wųø=VŦXXēú”øĸtđt˛]zü„‰cG)̚)RĻÄŗ§O!̘ōL˜:›6 î#ļē´kÕ+–+˙-Sæ,p‹KŲ‡*c”.WÛļlúHȞ9uß5¨§O#f˘ø<{xŋ{‡ׯ)vŠ/ËWA^ˊė™=}*õũ I“%Į™kˇ"kŽü\8æũ,^ŋzĩ(d­âÍFö%`–zi_JưN!kŒ…ŠUĒaҊŋC ŲΟ)ߋũn?õEÂD‰đüŲStnÛ;ˇmEę4ipôü•āÎdÅwĀO=ąú˙ų—! Yųl)Ų ~”î}ģwĄQH™*N\ēū‘ũ}Ū"ÔųōĢú}ž6d5uëŪƒČ7䤀ZË*éŸēr#ÜŦׯƒŨ;ļ…˛ßÔĢĨėí;h0~ėx\VاÉWõ”ā^ũ GŸ~Ÿœ˛ÁB6ūŠŽí[|¯ėŽYˇf/\bÛ? YÛ¸ą—ĻĖR/5…bPc˛MœnSČÚÎŨĖR˜ĩ˛ErgSö¸ö2ēõø¤ ĘąėEŊ|ņnŪ¸Ž׎)BXŪÖĄ›9e{tî€Ĩ įŖLųŠXļæ̧ˇˆyõr–ė艓­îĒ!…ŦmÜØKSfŠ—šB1¨1 Yƒ&ÎFˇ)dmg„nf)ĖZ YšŠ*gÆ4ĘĮųs—ü…j5k}RČĘ V#R„kČGļäș [7mŒPČnŲsyōåPČĘņ_r ØīS&aH˙ž¨RŊæ/[—ÆÆ JČJž/_bõæm(Vĸd¸ũĻüoœâĩZĩ1wqā‹d=A['IŠs7îX5ŊEČįΜNņcø˜ņhõCĢú}ԈBÖ6nėĨ)ŗÔKMĄÔ…ŦAgŖÛ˛6‚3B7ŗf­„ėæ ëŅōÛFĘKV'/_WD¨<áZ°w×N4Ž_[Yu-]Ž<Ē׎‹ė9r"{ΜH–<í߇/kT‰˛]8÷ôéÖå“gą†ģ"›ësÜŋwOyŅ,ĸYą"f­Y‘•įōÅ +åĖÖđN-Ȝå3l;päŖS Bžv°fËvåLŲČY•­Vļ|||ФyKŒūߤ`1{äĐAtlŨ÷îÜQŽ +P¸ˆō"™ŦЊũ…Wsmz(dmÂÆNÚ0KŊԖŠ1­QČ3oļzM!k+9ô3KaļFČÖoø5FŽŸœ•ūāõę5Ž\žˆ•Ë–_6ŪĮėá Ųī}Ĩ\BP°peĩ1čä9Å`˝C!{Jå‰7.Ž? <7VĩBVú˚6ŋüÜ[YåœŊp1Š—úBą%/—ĩũž äv1yB Yš„ |ņBxp˙žrSØø)Ķ•ūōHûvÍžSÎģ•ŸÍ[ēÂęŲēxū\ôîÖEŲR‘'tîŪ_”)ĢöĮ)DČJuĐ3tÔX´éĐÉjûá6¤?öք€YęĨ&0 n„BÖā Té>…ŦJ`Fjn–Âlĩ&/˛MāĶ~˙hõ26ūžyđmdj§ëË/°xÁēwxD*¤U ŒÍíAĀ,õŌlŒf“BÖh‹šŋ˛Qã§ëŪf)Ėö˛ō‚Tņ|š”ˇķŊßŊ ŧäāĖč×á9Ũŋg7š6Ŧoooe_oÆL™áëãƒ;ˇo)ûOåˆ-ŲrŖ˜) YSĻÕhA™Ĩ^ģ=üĨĩUũÚ¤Õonĸė™Y ŗ=„ŦäߎY<ö 9r寈ņ•—œõÜšu 3&OÄž=ģq÷Îmå%­ 3ĄfŨzhÕŽšģ;Ë5ûK!kÆ!RfŠ—‘ę (d] É!B¤5qžÍR˜í!dMœvã…F!kŧœ™ĐcŗÔKĻFuH˛Ē‘ē…ŦĄĶ÷iįÍR˜)dM˛fË(đ=€éâ[B ´=ÄtK÷Bí5ã‚~fSôA…šÖÚąˆ• üũņėĖ52ų:6DúĘEáûę üß@œ$‰lC‹N¯īŠHÍŲ˛6R•ČķQv‚VdÅvDãŧēq'&,†ŋßČmEŸ}Yé+Åöv#ņîņ ¤,‘ÅļRDíŖ#įáīû|ũ¯s#¤*žW–ũ‹o)˙ÍĪë­bCÚË*°ˆûĶĶV(}ĸĮŒŧ"YžŦxyåÎL[ ŋˇŪĘŪžlMĒ!mš‚Qž]f)Ė^Ŋkāī_&Ę@h@ŸĸGŸ—&QÜŊút.”Wš~‚$dY/Y/ 0÷ åĸ-BöWŨ,°Ā/ŧ ~éËP¸ álIŗD)+˛’šTū„=…ŦįĩģØņÃhÔÛ: W–lUļäīō5v˙8EļTļŧyø [›FíÆãõ­ØÕi*ũŅ 3ĻÂí-‡pcŨ^”›ÜSŽ^wŖč€–xīã‹=Ũ'Â=KäëÔ[›Aą_Ú(ĸđõ퇊ũrSz!AēĄRČF4ÎąQ ā‘=Ŗ"`ß>|†ķs˙A‘Ÿ›#äŠėĢ›÷qzō_(9˛#bÄríMqķŸŊʘ"dEĀWšŨ1bĮÂņ1‹;i"ähZ›Dҁ­a.âûôäå(ķ[loũ+Jüúƒ"ÎEØīę8%GuDÂô)Ŗ4ÁĖ"dŖI@;v˛Ŧ—Ŧ—ÚMU×ļ¤FȊHŊ`€EŽM×ҟß˛GVl7+,Ûš0ßÃîĮŖÎú ĄVdeåņŅĄsđēû¯o=ÄŨíGQķīҊp<Ē.Ę'´{{MBÂô˙‰n?¯wČŨŽ>ŌU(Ĩ‰E!%|ėLa °^†ŠËŦ—üGĸGj„ŦŦŧĘ[ņgô}RČÅņë< #`ƒđØĩ0ßX쎎BšÉŊ‚…lîÖuQšĻ\A$ÍķÜŗ¤Å–īŖæĒQА=:bžōBV°ĩ|ŋŗãXälQ )‹ Ų]xyųŌW.†s3W‡˛ÍDōŸãŗ¯*D(d#G:øŧ|ĮG/âņą xtø<*ĖčĢŦôí‘•í ‡‡üĪV€ûgéđū­7.Ė[Ęs(ÂU^ zŠ-čûŒÕKâčˆy¨ēpp°Ož×ī)øØČų¨žL>Ø|ŧŸy"Vĸø˙ŊcÆ2oßŧÉäįë›ÆūCF>B‚ ŧ^zzúē{x}pīŪFs"īÅá`~]gZPČēNŽM)…ŦŠŌÉ` ĩã”H’,YĪwoŪiŲŽ}ŒšuëÆI’4bÅäÎĸG†Wžžxúä16Ž_˙rҜYą|||ęØî|īŒáķkŒÔˆ+ŗO æ×˙hŒa’BÖyĸ—aPČrJ˜™…ŦöŲ͙*MÚĮ/\q×Ū´ũ->öEre{ãííĀūŖræ×iĶÄi YM0ԈŖ PČ:š8Įs$ Yi'Mž|îđŅã[ÔkĐPcˎ37ā§žžsfNī`žãF5ÆH˝1ōd'/)dí–fíK€BÖž|iŨš(d5æŸ)s–‹[ö0ۊ IDATĖž q4÷ė܎î;,ŋīÎ7ã1ŧ9æ×đ)ŒJ˛QĄĮžN#@!ë4ôØ(dĩ…ė;vėG7ŋĐĮŅ6ÆvūėÔ¯^ųš×ë×Ym4aÖn˝Y3k]\˛Öqb+ ÕYBčŽĻ(d5ʼn ŠŌ¤9püÂU]œkkh÷īŪ ¨]Ĩƒ‡÷īĨĩՆIû1ŋ&MŦ•aQČZ ŠÍôE€BV_ų 7Ú Õ–gÆDîî'/Ū~`ˆ#ˇ" ũîíÛ•K÷|åé™X[<†ˇÆü>…Q €B6JøØŲY(dEžã:‚…Ŧļ”)t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ…BG[žzŗÆüę-#Žõ‡BÖąŧ9šF(d5I3ē$@!ĢmZ(t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ…BG[žzŗÆüę-#Žõ‡BÖąŧ9šF(d5I3ē$@!ĢmZ(t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ…BG[žzŗÆüę-#Žõ‡BÖąŧ9šF(d5I3ē$@!ĢmZ(t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ…BG[žzŗÆüę-#Žõ‡BÖąŧ9šF(d5I3ē$@!ĢmZ(t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ…BG[žzŗÆüę-#Žõ‡BÖąŧ9šF(d5I3ē$@!ĢmZ(t´åŠ7k˝Ū2âX(d˛ŖiD€BV#4ŖK˛ÚĻ%BĄķāū} ęÛ {wíÄû÷īņyļėčųsTĒZ]ņ kšäØ}ä$Ō¤MĢ­GaŦ>°ũꁭ{F8ÎŨÛˇ*—.îųĘĶ3ą]1žqæ×x9ĶŌc Y-iŌ–ÃPČ: 5r YmĄG(tžkPŸ}ū9~ūe(bĮŽÛļĸ]ķĻØ˛į˛|–׎^AÆL™3fLm=ĸÕ’'ķĢ%MãŲĸ5^Îč1 YN3 Õ6ģ åKŖņ÷ÍŅŧuÛāˇl܀\šķ ]† ĄVd§ũö?ü9oâ'Lˆ’_”ÆÆÖâ𙋘>i"N; oŧ|ų˜>g˛Š{æÔI îׯ_ŋF´hŅĐĨ{/ÔŽ˙Ĩ2ÖÔ˙ĮâķČŨ9råÆŲͧ¸"k[Ū™_Û¸™Ĩ…ŦY2ébqPČēXÂ],\ YmĄĐ9~ôēwüA­xÉR(V˛ĒÕŦ„‰)˙-hkÁĩ+—Ҏûøgû.xx$Fīn]°kûŋÁBvŅÜ?đīūÈ7.ztj)SĄk¯Ū¨X˛(æ-ũ ŲsæÂķgOQĢb9,\ņ7Üģ‡ŸēvÂĻûāî၎íÛâšŗ˛ļåųĩ›YzQȚ%“.…Ŧ‹%ÜÅÂĨÕ6ᑾ tõō%Úŋ{wīÂū=ģątõ:äĖ'XČΚ6nnąĐođPÅ3Í7 ˛—.œĮÄé3•ŸÉ í­×Ņ q|Sˇ&>û<[p4žž/ŅČpœ9y~~~2rŒōŗÃ ¯î˛ļåųĩ›YzQȚ%“.…Ŧ‹%ÜÅÂĨUŸđôîDĐ-\ĄķčáüŌˇ7&Κ77ˇāŽ"(-~;!XČΙ9Ņ ũ‡ SÚ]<Íži,do^ŋ†Ņ' Yųū̝ŖS›8zūJ°m3q’¤3||}}1tÔXåg'ŽEīŽ)dÕį]z¸R~E´=˛ “i{QȚ6ĩæŒBÖÜųuõč(dÕĪ€Žč āT˜îá 9Ĩ |ąB¨Xĩ~čô#RĨIŲBĐšm+4kÕM[ÂÚŗsŦßą‰'Á€Ū=ąuĶ:}AY OČ6Ĩ æÅāŖQ{ãú5ȞÜu[wāé“'øą}lŪĩI’&CŋžŨpäĐA YõyPȚ,ŋŒP@>mCeĘ^˛ĻLĢųƒĸ5Ž]9B YõŲ  ''ÃÚ?zžsë† ę‡}ģwã×k¤N“M[´BĮn=”—ŗBŋ5{úTĖ˙câĉƒ\yōâԉãØyčX„BVVh9ŒĄũûÂËË ūūūøąįOø˛Ņ7Jts~ŸŽ|%H˜ ÁŅÇ(dÕį=B!+?0A~ƒŦœ—ĀJ mÃdÚ^˛ĻM­šŖ5w~]=: YÛf@@ˆn^!íËDîî'/Ū~āa›Yāô‰ã8t`?Úv”_`æÔÉ8~ä0fĖ[hĢIÕũxŽl„Č"Ũ#læ7Ŧ€•dNįp3˛x\ėį˛.–pŗ„K!k–L2ŽđȆMYU|L<Ǥ“ƒÂôđ° ‘ģû×Q˛^¯_ŖW—ޏtņ‚˛R›6]zŒųm R§IŖĘÁ¨4![ļhwŪŪŪ‡ŖbGŖžō˖ėEÅLėXąc¸ųøE\[č%ŋ– /ڐߐ„qČįŽeûŒ­ašĩŸō@)ŗȸĖI€B֜yeT˙Č ¨"0+Œ•,Ų/;4‘ģû˛¨YU^ØŠą؊ĨŠžözũēž†Pcö;ËG܁‡â:÷I?A‚™Wî=<3Í Oˆ÷ƨfŲNŅ+zČŊ _p_ŽŅ'ˆˆ…,į @HōĖ`EĀĘ>YŲŽōGĪz@­ŗ­ũt\l˚ßL–ŧ‚­übÖĀ<0§ $@Q$@!E€ėN&#pҞ‚}<„€ ŅŦBĮ™)¤Õ˜ū'~Q ´5ŧˇü˛Ļņč4G$āh˛Ž&ÎņH@ŋdvˆeĨJV`Ã>˛ÚįŽBVcĻVŦ¸‹ •“9~åņ[ç9p Y'@į$`PĒ„l‹Æ ąkû6%T垄čŅŖ+ß>{ÉSČKŌÖ?rLW‡–Ͱ˙äYë;…ĶŌ Ą%û*;VČ2ŋ*3Íæ$@v!@!kŦ4JĻ$ JȆ$P8gVLž9Ĩʔĩ …ŦÍčŦíČüZKŠíH€tC€BV7Š #$ {š V˙“'Âīũ{<{úÍ[ˇE—?AŽp’ÛēîŪž­\q[´x åšÛBV.=čĐĒĻ˜ƒâĨžPŽ+˛âb~UÍ$6&Đ Y=d>€1h&tüüüPŋZ%LŸŗ2eRnŽ*U0.ß}„5ĢV`ۖM˜ĩ`1|}}ņĶĐgā`ÍĨēŗöô$d™_íķK‹$@v&@!kgĀ4Of")K–{ŽžĘ#F ÆuøāüøCë ˇoŪŦĨƒ ô$dÁüę`FĐ U(dUábcpm‰“&Ũ1aƌōÕjęAږ‹ÁũúøĖœ:y €ąļYĐ´—Ž„,ķĢiniŒHĀ(d™C€‰T*RŦøōĩ[w$1jL“šûųųų%ā̃t%d0ŋ:˜tHĀz˛ÖŗbK iĶĻ]ÚčˆÚõ6Šo4 ŋüÜĮsÕ_K={ōd’N|כe~u21č €u(d­ãÄV$@!,Rtk۝KIĖöúąĶĢ{ˇoßĩc‡ļŅԝĮ˜_ūs'0  YŖdŠ~’€Î¤M›~Yęti+wčŌ-Iåę5āææĻ3—/^`ûÖ-ŌŋĪk_ßņ¯^žĸ3'u)d…ķĢŗ™BwH€Â%@!ˉA$’&M6 Ąģ{–7^¯ãûx{ĮŽŠ1­ú&H˜ĐëÉãĮÉĶĻOÍËËkĪĶĮG¸Ļ•} íčVČZbd~5L6M‘ hO€BV{Ļ´HŽJĀ€ŧDĨ‡'ĀKoôāĖ'|Đģ é:ķĢķÉD÷HĀ PČēbÖ3 €^IČę…ũ  `˛œ $@$ā<˛ÎcĪ‘I€L@€BÖId$@†%@!kØÔŅq = ÕCč €Ģ uÕĖ3n MPČj‚‘FH€HĀ&˛6ac' $@!˙@$@Î#@!ë<ö™HĀ(dMD†@$`X˛†M'Đ Y=d> ¸* YWÍ<ã&Є…Ŧ&i„H€l"@!k6v" @˛œ $@$ā<˛ÎcĪ‘I€L@€BÖId$@†%@!kØÔŅq = ÕCč €Ģ uÕĖ3n MPČj‚‘FH€HĀ&˛6ac' $@!˙@$@Î#@!ë<ö™HĀ(dMD†@$`(î<-SČ*u.álCÔĀ#˜āCęø @vīėĐ Ā}ĨːĮFzÃė°ÕÆūėfr˛&O°ÃcÁŗ3`š7%Ōø@yŨ$°@Ë˙ø™2rĨw °Ā O-ât6€Ûž '€f~ĐĀ.‹ø@Ä­ˆ×âQ˛{ °IīđčŸsPČ:‡ģFeÁ3Cƒ3ô0Ę2°ŦĖēˆgųū-€øÎpŠc’€å—džžCC-DȖō—Ŧčnč`uˆ>ōßÛX W!;@*-måûdē 6?´đĩåßĘ=˲Ō+ĢÅ5-[#Ī[l=āeņĄšÅÎ!fÔ5PČēFžíĨŦ"ąā؃,mēoą-„x_Ą%€yŽ€1ę’ĀJ"†ãŨ6‹PœâgiÜābģLØŽ!ˇD$dp@:ËÖiwĀf!WdP@+Ë6‡Ū–U_Y–S=Ė´üĖ_—„锿(d5Gę2Yđ\&Õ ÔdUvd˜neE)ĄÆĸI°–€Ôõ DH†}DhĘ ĢˆÎ 'ĩelb/#Ä!+ûieŦ|![6Xļ)ˆÉBv €ĪCˆæ˜Ū(j˛˛ú{ŨÚ`ŲÎ(d͑GgDÁ‚į ęĶLBŽĘJ\\5SvKËĮų%Č'˛—[„b,×,{ēemČį&€ŽÖ„ųīk-Ûâ„ØZĐßbŗ“Ĩ­ŧČ%ŋŧÉÖŅ#˛Ÿļ’eŋíßäeȐBVV…[^>˛%Gļš=ļYYŅ•}Ŋ|\ˆ…Ŧ %[ãPYđ4Js.G äĒ,Wc].ũē XDĄ|¤//mÉö‚bd+ĖĪ́Öqųl°åE0˛Ē*ĩŽåcY- :ĩ e[€ėÁ•ąvZÆZh§ĨČ>qŲC[Ũ˛×UÚȉ ˛Wļ|cÔ¯L ĢÁßRČęr>9Ä) Y‡`6å ,xĻL+ƒr0 UYŽÆ:<‡‹€ŧØ%§ČОâ)GhÉ*l6Ë["VÃ>˛GUVU3Xö§ĘKYrם0ĮoÉęëŸ X^“˛bXúĘÉōŪ…Tŗō˛˜ėו}¯ŋũąŌ7čĨ0Ų{ @ō˛—ü[âŠŦ Nl YLē†!ŗāi“Ļ\’€üĪYŪø–}|H@Ī>ŗŦļĘĘ,Đ  YŨ¤ÂTްā™* ÆFōŠ…ü[Č*&>‹d˙ÄųÄ{Ä â§^?âGŪÄŪÅŪÅŪžŽxžxsÕ˛?QūŒč@z]d7 06 YcįŪ“ č‡@~9g͍›HķȚ đÉDĪÄE+oŊČUIōymĐßå{ųUžä­ų’ĪHå9×č đęķ'pōpį) /ÂČ>Fy҆  ¸, Y—M='Ѐ@Ŗ”@Ŗį@íT‡/€…Č—ŦŠõ´Yņā{xī øÄ–<–YŽ/ŌzHÚ# ] Õuzč € äMt÷ž+ø5âįŗŧ‚íh_8`!āuđĻzSårāÍ?@<ÄeO7ļ ė~”Ķ` Y :Ķϡ`ëÆ Ø˛q=R$O-š#a„H:5RĨJüįÇņāÁũéųęæĪ›gĪžĄrš¨ZŖ&ĘWĒâĖP8ļI¨Šë˛&IēĢ…ÁÂëj8^5īÔ˛ĨvlŌ¸Ųë@ĀĀĀgÄK!ĢDg˜XŊb9&ƒ é3 ^Ŋē¨[ˇ.ŌĻU Å;w°nŨ:ŦYŗˇīÜA×Ū}Qī̆Ήcš„€šēN!k’¤ģJ,ŧŽ’iëãTSđ>a5c4āÆ& Z¤{[īšn[.&īßą4p’BVˆŽ4ąsÛVŒøe ōä΍ņãĮ!Mí~QÛĢ×O¸xéú †˛*924Žeję:…ŦI’nö0Xx͞aÛãSSđ>%dcW†nŽđÁč/ļī|ĩššŒBÖöéëđž}ēuÁŖ{w1fĖhäĪ˙ņŪVoooŦ]ģĮĮéͧƒˇäȑ/^ Ūj } *„zõę!VŦ:qâz÷3aä„ß'465uBÖØšv īYx]"Í6ŠĻā}JČÆÎ5âkņö“ÍÁ8¨cW ā$đú6ˇ—QČ:(oQæųķg¨ōEq,ūķO”/_ū#SķæÍÒ%K°{÷ne‹Aƒ B퓕Ŋ˛˛76hėëׯąbÅ ŦYŗ*TŽß~‹æÍ›dwûöíhŪŧūŨ‰ÜMwY^TRžŸ  ĻŽSČr*é– ¯nSŖ+ĮÔŧO Ų„ĀÉ*€G_é*Bm 0 đ| $ÖĀ:…Ŧíib˙žŨøĄųwĘ kØm‹-BŸ>}Đž}{/^UĢVUíĘæÍ›qčĐ!˚5 ŖGF“&MBؐíyķæÅŧĨ+PŧÔĒíŗƒëPS×)d]o~"b^C¤INĒ)x‘ Ų€Gg5ÔŌEtÚ:1@ĩ€&˛ÚÂÕŠĩëW¯ W§ö8p`(=Š–-[ĸ@ŠøÔbŸėŨģwQ|öėYČ oÁ‚CYŦXqLš9ŗdŅ)-ēĨję:…Ŧ^˛F?‚ °đr2¨! ĻāY#dĨl/Č  §GtÜÖ@Ĩx  YgK;×dA`Üđ!‰ØšsįbÕĒU9r$ōäÉŖŨ€K˛ōÛģwoeeļYŗf‰ŲūÃF XI™|H |ję:…,g‘ްđę*†pFMÁŗVČJģ%Öh  ĸ!H„īäęĀ3cņ?A—=PČ8ĄVē.[ŗĘ)€§OŸ†ęŅ­[7ŧzõ sæĖąŌ’íÍdĪlōäÉ1nܸPF<<ÍāØąchĶļ6ėÜël×8žÎ¨Šë˛:Kž+ēÃÂëŠY×.f5Ok!ÖŪË*Š|´€ŧf$Båî#YEõ€ĀģaEÔĘ;ãgČÉ"`ŨĐ@ō_"ˆe[C&ËWTRČF…ž~ûĘŪģˇnƒ%˛&đ‘ķ[åĸ{œN –Äɓ'•#ŋäÖ¯ §QŖ¯QĨv=ÔųŌ˧7Ģ%Åöję:…,į‹S °đ:ŋ)WSđė-d­*‚WD̈ZŋĸV„mdEŲÚ1>ՎBV ŠúŗQŠdQüģuKđ ^r؁\V°páBŨ8+7€Éĩļ7V|’ŋjÕŠƒ-{ęÆG:â|jęzd53#€],Qá 0Οēō€…WWé0¤3j ž^„ŦŗASČ:;ڏŋãß-X0s6mÚlCF ö+*VŠöŅPÍ}‰ŨēB.§f͚čŌĨKđ÷öō-*veėôéĶņĪ?˙(fäĪ)ĶĻcîŌQ1Ëž&! ĻŽSȚ$éF Ãė…Wn+ëĶ­ šļh.=2Zz 寚‚G!K!k¨ÉmqļKÛÖXš| <'Fâ$IĐđpÔŦ[/8”ęeKaŪÜ9Á7f,Xr-mĐšsg˘1CŲ‹-Z4ĈC9ūę˙ûräČaŽ āęÕĢH•JîĢŗí9~ü8Úļm šŨKųžUë6ظkŸmŲËTÔÔu YSĨŪ8Á˜ĩđŠ€ܯ?z¤ŧpąëĐq¤Ī(Į1ķą5Ok![Įrult‹aš‰Ģ9ĩw—ã“Ŧļ¤0vyü–ƒ@k4Ė‹ĪQ0[øúĘ5@Üxņ(‘; uęĄdū\8yâD°°L:ĩr{–M˛"b'Nœ¨ôõķķC§NpæĖ8pĀ&ĩ˛<@á…q˙ū}ÅųžPáÂ8~ņšM>ą“š¨Šë˛æĘŊaĸ)˜=‹Š oHûōÅ %ĩęÖĮŦ…˙]i˜äĖQ5ĪBv€âÃ'ü` 5kUŽ(dũũũņáÃåËßōįûīƒ˙ūáƒ˙÷Ũ.¸ÅFPåŋ[ÚÚöĮ‡÷īĮņ˙oœOõkÃ_lųŌv°Ũ~†Œ'”΁6BúŌįū6Ū[l*? Ķßëõ+ŧ}û6ÔŽ;ÆNš‚å.Āž]rwQāSĄBėØąCų{X!+˙m˙ūũĘļOOOTŽ\íÛˇGÆ •öōŊôŠ_ŋ>úõë§|ä/' ˆ8–ŗi“%K˛mÚ´Qn {õęzõę…îŨģ+14rŧV@@råʅ)SĻ I’$¨^Ŋēr’Âöíە=ŧŌ>¤Ÿ2v™rå°|Ũ&ƒU ēkję:…Ŧ=2@›‘øēNuĶŪ'OžâŅð|üø PąjUċ?Rl5oŪŧÁÆuk|>|ø°$ –â'ĒīTmh–Ų°Bļ§eeU •P€Ŧ{Éz˜\M+ĘĄHrčP{Ĩ„˛§-ĢŗÃ°Ā"ūûŲäĀ{"œeũ*'€1*‚—Ų¯_?@6$ưņKBžąŖGž%]† ŅūĨáB…Ŧō÷čŅŖ+sËWt˟1cÄ ū{ŒŅ˙û{ô˙Ú÷ §ŋb+z Í@ģŅ•ÕČáõ—6AãûƆb/Ŧ ąkąaU˙| á§Åņ5ؖŇĀīŖŖHŽlŠĐUęK‚VŋŒ…/}ƒųrZŊ0 âĩ[ˇnxüøąrŽkDB6ūü(Y˛$îŪŊĢ0>|8Š-ǜO+BVÄë/ŋü‚K—.!_ž|Š(–ļdĢœ9s6cƌQ^čZ°`"d=<<đįŸ*q<ūœ+˛*ūÍēZS YW˸ãUŗ"Ģ÷Â[ģūW˜7ëwĈ>ŪŪÁ˙ŖiŅϞfËnĀėËå/_Œ<đ­Ÿ¯oį(xž4!0hH ˛/-j΀Eœļą) ›éŦĨ50Ę"Poh `./‹x @ž¤ė^ŧhų~ļEĞđ3€ĩæø€¨wQ”ąT8.Bļ!đÖ'pYôĩ-_ĸǤ_ō‰møwĪÁx˙‰Ō@‘Ēˆ°P‚ė?ņĻÂ]—oúû”I1ddåI¸úušˇnĖ%˛­Z"0ãÆĢ´—ÕÕRĨJ)+ĨéͧPČÖŽ]e˖…üĸ("T^ +WޜbC„ėųķį‘!Cå{wwwåû–-[âʕ+Ę÷ōˆ`•qåš\ą!§(ŦĀ=˛.?­? €B–ķC÷ĖVxe?ėˆÁC ÚÜyķaëۃēĪ…ŅTSđ>kƄĀÉ€‡aWd/č ‡|D @„ėréđ›eu5hŒŪŠČ@. ¯đČĒĢ`r!+įY€/Bˇ— ?Y‰ŊFŦ>Ō˛"û€ŒŖöĄUKĖyíåöĢ5+˙BãĻÍ"tĸgį¨â؟ūY9Á@V@#˛"<åZÛącĮâ… (R¤ˆōņŋŦ֊ •ũ´ņâÅSVp7mÚ¤Έ„ėâŋ•Kvî܉„ ĸcĮŽxņâäĖØ°BVūÛšsį”- ōđYįÍ1=ŽŦĻŽSČę1ƒ.ā“Ų ¯ځŊ{"OžühŪĻ dÔy!Ē)xö˛w,[ dīĢŦֆ˛¯-[ Æ( ÷Ë[N8ą*īĸZ°Āī–•XŲ˙*û%dA&[,bW^&“}ŗ˛Î›wzyæ´)pãĻė}•į¯ŋūRžä†¯Č„ŦˆÉf͚)/mÉ^WŲ [dEļ˙ūXąb…"JEĖĘ1^ōWDB6eʔÁ/ˆÉčėŲŗcöėŲÁ/{…\‘•Ŋž˙ū{e•WžqãÆá?ĐĻ}'=ĄĨ/N" ĻŽSČ:)IŽ>ŦĢ^ųhNÎnäc?j ž=„Ŧ|Ô/ĮoI–åÕžē–m2VH!+ߟ°QyŲKúČ=LUÂŧė%ídĨVvʋdōÁ̈Vą/ģåƒeŲ?+/{QČÚo^ÉōŲͧđSį8yRf”cēD|úøČŽm}>˛×WŽÁ+OžĪuäL0æXję:…Ŧ1slJ¯YxM™VģĨĻāQČRČÚ}Będ€Wžž(ž7^ž|Ę#Y™}üøą˛ŨĀŪl'#ĀBŽÄʘ˛2|ęĘMċĪģ‘ėŖÚWS×)dšeúÍÂk¤: $5B–BÖSR7CÚŋ#~éÃ‡Bm%§,Y˛#FŒ@4÷WN'čͧZļljOŦ T¸p =EŠ×|\45uBÖZĖĘi­ZĩBŽ90zôhdȐ!ĘŪʙ°"`¯^ŊŠyķæŸNdXDėôy ‘>ŖVˇŪGŲeĐ)5uBV§IteˇXx]9ûęcWSđ(d)dÕĪ0ã÷8|`?šͧOŸūH°.]ēTŸ-Z´@ņâÅQŗfMÕËĨ ‡Vļ+ˆ(ēė Ȑ뉕#ˇ–Ŧū‡+ąĒéēf5uBÖ5įˆîŖfáÕ}Št㠚‚G!K!Ģ›‰ë`GdëV•/ŠcîÜ9¨TŠŌGŖ‹]ļlä4ēuë*ˇošģģ#uęÔÁ_<€|Ũŋ¯_ŋÆ_ũ…5kÖ Fhܸ1š6mú‘]ą×î‡öØļ˙0÷Ä:8įFNM]§5rĻMî; ¯ÉŦQxj …,…ŦFĶΰfú÷ė†[ׯa˘Ņ(\¸đGqČ1Yk×ŽÅącĮpęÔ)E¸ĘWöėŲqéŌ%EÔĻI“rc˜ô¯W¯ĸGū‘šė wī>Ȝ-~Ë#ļ ;aœä¸šēN!ë¤$qXë °đZĪĘ[Ē)x˛˛Žøo$lĖ{vîĀČÁ‘õŗĪ0~ü8MöĮ!ûd{öę…7oĄßāaŧė€Î&ję:…ŦMˆŲÉŅXxMÜ8ãŠ)x˛˛Æ™Ųö÷tŨßĢ0e¤HžõęÕUļd˘QõžvŨēuXŗf-=yŒn?ũŒZõęĢļÃ$D@M]§åŧ1^CĨË!ÎĒ)x˛˛™”d÷ŽíØēqļn\D‰ĄE‹æĘŸAÛ‚öɆÜ#+éé‰ųķæÃëÍTŠ^UjÔD™ō =ŨÕ#5uBV¤O‘`ፑË4PSđ>%ÃįĀæ%@W÷čœģäÕ Ūô‰_8ķOˇ×ĻŗM\8wû÷ėÆĨ įņäŅC@?Ā͈åĈ øĨnŨ2ŊÜä{7Ā× x|@Ļä¸ķüĩęžė`,j ž#{ĀĀ3ÉtčŸ5.QČZCÉāmZ4nˆ&Í[Ąjš„šēN!k„ŒŌG›¤KœˇŸŊBôčō .ŗPSđtÆ@ļČjl^~0Wg>Zã…Ŧ5” ŪĻmŗ&¨ßākÔĒWßā‘Đ}#PS×)dQúh,)“āü­ûˆĮw1Ų";PSđtLöŊ„ŧ¸^žw摴ļ⥐ĩ•œÎûŊ|ņ‰§¨\0#ĢąõĘEq€œÕũYVyŸ‘ hO@M]§Õž?-ę„@öôŠpôÜe$L”HņHļЃ‹ø˜Š€š‚§ŖĀ[˜hY rKVeģ˜Ŗ#?­q…BÖJk#ī”-ZyķĀOũbÆäßPĒLYČ}ģ˙ˆĸ%JáĪ•Ģ Ũ5 5uBÖ(YĨŸ‘‚ÉãĮâ§~‘6}zäɒģĮģˇīĐĩ}[´nß5ęԍÔ‹€š‚§ŖČdõ5€˜äˇĢŪxcĀUY YM,-]Ōŋ/ū˜1 ņã'@ÜxņđîíøŊhŅĸaÉßëP¤Xq-‡Ŗ-& ĻŽSČr☆@@@ŌzÄGŧøņQēly?zÅJ–ÂÎm˙âÃû÷Øuø2dĘdšxH 5O'ĖĒØāWƒø[Äė/ˇi6ëÄWkÜ ĩ†’AÛHM•Úō‘ÛŊļî=hЈčļ¨Šë˛FČ(}´šĀØ_‡aęoāëãwxž <;žJõ˜ŋlĨÕvØĐ8Ô<F$dCĢ:Ž[˛ÆÉ•jOĮúUų¤Ë××Wéëá‘ĶæĖã1\ĒI˛ƒję:…Ŧ˛lk™S$ÜæøČŲ•ë7#Ožü†đŸNĒ# ĻāŠŗė°Ö˛­ĀͲ2ë°A5ˆBVC˜z4•5u2ŧ}+—'CŲļuäŦ\0Į‡ėG@M]§ĩ_hŲIæ˙1 ÃõĮ/ywĘŨāvėq’7ÖŪÔ<{ûbŖ} YÁą›c,œû†øūūū˜8m&ę|ų•cæ(.K@M]§uŲibîĀsgN‡Ο#‘ģ;æ.^Ž’Ĩ˘;`ŽNMÁĶ)&yÉ+ļå…/ēøIˇ¸"kÄŦŠô9gÆ4ʙÜįnÜUŲ“ÍI@=5uBV=_ö0Õ+–Ŗ{Į1sfė´F|Ŧ^‘Ŋré"žûĒƎƒŧyķÂŨŨŨˆņŌgĐ=šüįúõëĘWģvípîæ=č+üãQS×)d#É) ¯1&=Ŋ4>[ ¯š‚§SJ>r¸€ĀķŒ÷X%dĪž>….mZââŠƋ“€ <}ú3eÂÕûO …šēN!û‰´˛đfÎĶQ“PSxÕ<br !Ûžå÷1trįέĶ4Đ-0/šķæáÜĨ+čųķCŠĻŽSČ~"Ĩ,ŧ†˜ītŌ¤Ŧ-ŧj žNQÉĄĮōģZ#>V­ČĻq÷Ņ QF –>“€ ėŪŊ?÷€ëqi šēN!û‰ÉÂkÄŽôŲ,Ŧ-ŧj žNŲŧĀˇxčÔŅ܊TČŪšu+ ]ķ&8yüxd˙Ī1Väô– BāîŨģĩëüŸŊĢęčĸ'¸'Xqw(^Ü­ PÜĩ…âPŦ¸KqhĄ8Å-Ü%8MpKˆá$HūīlūMH˛ovßîž7;ķ}ųĖ›š÷ĖĖŲŗ÷Ũ™qÅŽ#'tąyxŨ”CY Öõ& O+sB¯VFBÚá¨(%^ÂĶ(– Y߇ÃÜęÖÄÇL}æhtˆ¤Y}#đāÁƒ° *âôU]ŦA^7åÃ YIŧú^´Ōzũ# ”xyOŖ¨ĐŨŸŠ Õc1 |ĒĮa•6‹„€R>ՊĪ<ŧ.…l4Ŗ&‰W+ĶYÚá¨(%^ÂĶ(–!Ōøz™ŊūОú3iąƒ! ”Oĩ ¯K!+…ŦVæ­´C"ĨÄËCx…X YŒ4K" JųT+ūōđē˛RČjeŪJ;$Ž*dßH €­‹ŒČęqԤͅ€˛¸ŲKĻ8Ô—ÎjĨÄËķÍ]ƒn’I¯¤@‚VE Y=Žš´ŲĄPʧZ…‡×eDVFdĩ2oĨGČJ!+įžD@"`U¤•YĢN0Ų¸D@"đ=J‰—į›ģFq~ ãˇČŦFMŒŅ,‘ĩÁ¨%Ožīßŋg—J„††"~üøŦ×>>XŧxąÉgˇnŨŠŠS§˛úqâÄAšrå0fĖ,XĐäŗ˛‚6Pʧڰāáu‘•YĢÍ[IŧVƒÖ!VJŧ<„§Qā^Č€­‹˛6ĩÛˇo#WŽ\ߒĻTČ.Z´cĮŽÅŋ˙ū‹ĒUĢâͧOX˛d † ‚#GŽ H‘"6ô^ve.JųÔÜöÕ~އ×Ĩ•BVíų÷]{’x­ą(%^ÂĶ(PRČjt`´hVt|ēqãF8ũøņ#‚ƒƒŅŖG&6¯^ŊŠŽ]ģ˛(î—/_ĐĢW/tčЁÕ5Ddį͛‡… bĪž=Ȑ!C¸ÛôLƌązõjÔŦY3†ļˇoߎęÕĢŖ{÷îhŌ¤ ĢC˙îŲŗ'6lˆ‹/ĸoßžxõęœœœ˜MToܸqĀÜšsŲ3ôī'Ož`æĖ™Ė~zūÖ­[øüų34h€ŅŖGŗįe1Ĩ|j^ëę?ÅÃëĻf…ŧAŪDcņ •Äk1„Ų€Râå!<ųq- Z=‘ĩá¨Eŧ$^˗EĄf„ IDAT/ĩk×"[ļl¸˙>ræĖÉÄ# ڒ%K2aøāÁ >+WŽ ˛”°nŨ:xxx eʔ<šrå Š+†ˇoß"^ŧx~wėØ1&TŸ>}­%ņ[¨P!¸ģģŖ@L¨–.];wîĆ ĸ˛7FŨēuŅšsg&ĖIø’˜íØąŖ ‘Ģ+Ĩ|ǝyx] ŲhFMžZ Ūt–ÄĢ–ŽÔ’Râå!<â÷@6$hõX¤ĩá¨Exũú5ˆÅŧ~ũ:ÖŦYÄ# ÎN:ĄZĩjėĮÍÍ ŠS§fBvúôé ÄæÍ›™(\ B6$$$<'×P‡ĸˇÍ›7ĮķįĪŖ˛...ŦOĘã5”/^`ōäÉĖΨ"˛3fĖ@âĉ™;6{Œ„t:u0kÖ,"-VWJųT+^ķđē˛RČZ}ŪJâĩ:ÄBv ”xyOŖ@I!ĢҁŅĸYQņéŗgĪđã?˛Č%Ef .Ė„`PP­ôĒ~īŪŊØˇovīŪķįĪ3ĄKéƒfé$ZS¤HÁeCjÁŠ+˜¤ríÚ5]0`KMØącjÔ¨ÁŌš6mĘęT¨Pũû÷GĒTŠĐ˛eK|­[ˇfŠÅ$‘J’"ž”R@"”^ËĶŠ$pézƐ#ÛĨK&")5!rĄS (?•6{UĒT‰EtI߸qƒĩQ´hQÖåŌ’8õööF‰%X^-EcŠ?ОļjÕ d;ũîäɓ,RL›ÆNœ87oŪ råĘŦ}ʑĨ´‚L™2ąŧŨwīŪĄbŊ ģuëfC¤ÅęJ)ŸjÅk^—BV YĢĪ[IŧV‡XČ”/ái()d5:0Z4+ēT­6mÚ°]í$xæĖ&“%KÆĸĨ$cŊ…zõęąSŒ7{‘ˆĨ\ÖņãĮ3ÁšđĨt¯tüE\}}}™đ¤MZ”&Đļm[–“KŅ`:Ų€6”QēŠS§Xt–ú ÍfC‡e—ūM}yyy!sæĖ,w–6v‘ĨH-E‰IhĶ˙Q;'N”›Ŋ,˜JųԂ.T}”‡×Ĩ•BVÕÉUc’x­ą(%^ÂĶ(POäĸ7¨ĩĪ”Y2"k !Obu׎]¨_ŋž€Ū‰į’R>Պį<ŧ.…Ŧ˛Z™ˇVˇC¯Õ!VĩĨÄËCxǍ^cOč\{$hõX¤Õã¨I› Ĩ|ĒPxx] Y)dĩ2oĨ(%^ÂĶ(ÄRČjt`¤YQPʧZņ—‡×Ĩ•BV+ķVÚ!pT! ´z,2"ĢĮQ“6;RČYUq“„§R?7#Ī‘ĩBŲ€DĀ"”/Ī7w‹ ˛ŪÃA AĢĮb’×%ŸęqXĨÍ"! ”Oĩâ3¯ËˆŦŒČjeŪJ;$Ž‘^Čžyķ&Ŧfų2¸w÷ŽŠĪš $+ āååÖģo?§ĩÛ<ŦĐēúMJ!ĢĻ’xUQ6!°ĨÄËCx˜cÍG@‚VÅdD–œjR¯V,[~ČŊ•6KôŠĀĒUĢpäÄIŒ™;˛réâ°'ÁÁč;hˆŸŪ׎†9Ģ6nÂŋ°°0düáŨÎE^75`RČM҈ˇtĄŧaž¯"vœ8Ļæ.Cß_ģĸqŗæ¨PšĒ.í7eôĒĨ˙‚>Xú úÃTU]üŪ\âå!<áBVŖcaļYęÔĀ#FĄôOåĖnCË.˜3+,(Ā#ĮOōķâ˛×ہŊ{bĪ‘Bú§åš•m<ŧnj¤ÕÛčs؛)ĨsØŊ įˆ#¨íÖŽ5~nØõ5æ@E?U—-ū7Ŋ¯cÂô™ú1Ú –ōžēWŖI?% Õc1+"ĢGGc˛šNåō˜ô×l)öŖhŽ1¤rX5ë¯K!ĢŲa´žaĸ ŲŪŨģ |ĨĘhÚĸ•õÁ´CkW­Ā™“ž˜1oĄz×N—<„§Ģ#Xâ  4´z,RȨRĻ.]<ųōëq MÚ,…ŦIˆdāáu)dU^oM‰.d÷酂EŠĸM‡NzEönŨ´{Æ˛‡˙#ĀÃëRČ:đĖ]Ȏ2?dƌ.ŋör”IÄŽ]ŊK˙[/¤Jâ!<ĨmÚ¸Ū#e Õc‘YEsgĮŪcžē9§“wĸI!ˋ˜Ŧo <ŧ.…Ŧ%HëüYŅ…ėÄŅ#‘Ä9 zõ¨ķ‘ŠÚü#÷cáœŲXŗÅ]H˙”:ÅCxJÛ´qŊ‡h‡ Z=)dä͜gŽø i˛dzC“6K!k"YAExx] Y×[Sĸ ŲŋĻLħŸ0pØŊ "{O{žĀ¤ąŖ°e×>EõE­ÄCxÅ@ Y YYĶ$ĮMŋ ˆr^dßĨå™ ˛ŽĨđđ瞖ĸ­ãįE˛ fĪÄ“ā Œ;AĮŖŊé—.^Ā}ĮŽÃĮ…ôOŠS<„§´M×{ ´z,‘Ĩƒô3§J ßįoô8~Šl–BVL˛’Jđđē˛*ŽĮfD˛KũÛ7|0~Ú_z“6ßđžŽîÚâĐŠs&ëŠ\‡đ4ŠÃ}• Õcqx!û6$…seÅíĮÁz?E6K!Ģ&YI%xx] Y•@×c3ĸ Ų5+—ãėéS˜1w‡Į¤ÍîŨCķ†?ãäĨk&ëŠ\‡đ4Šƒ˛˜˜Ėzųâ]× į¤IņôI0*—.+wôú]$jOŪŋ~ Cd!kü;3™Æ-Uę4ėī‘/D D—´zuM÷vķđē˛ēnķ]ČnŨ¸{wíÄü—™’†Ÿ đGíJåpņÆ] [i}ĶxĪú֘ÕÃ=:† Z=‡ŒČ.˙wF ꏮŋũŽ]ēĄaę8zæ"fLžˆ•KcŊģ ëür„†ĩ̃xfüÔ¸áãÃnöĒ]¯žĶ°ũđömčfH=—iĮaÁŦ™øsüD+^‚Ũėĩdõ:§1ÇāØáCtË2eĄ{Ądą5<ŧ.…Ŧ­GĮŽũQNåô‰ã1nĘtö-Û dœh÷ôJ”.cG -īzΌiH•:5ZļmŨ;ļcũšUDLØļy#æũ5Ģ7mÕõˇėsgNcûæøcä„~ø:ģŌûÁcĐØöũĩĻĪ]@„l9:j‡đ4ę–˛SfeJéL7#âķįĪHœÄ!!oāää„lŲsāđéķĻ×üīwmw]õ+vlægėØąņîí;„~ şã&ĸ}įŽš÷!&_<Žby˛#nܸøō% ‰'{ōÆ‰ŽŖ¯Vŗ6VnØŦk˙ôl<¯K!Ģᑿ´ō¸ōdJ‡ ĸR•jØĩÃ=ŦŽkC§Cûö 44^7ī"eĒԜ­jĢúâķ0aôŸˆ+6jũü3čõûķ§Oáëû9såÆū§ĩe0§5ˇoŨDrĨŲ§kã&Øže*VІS'ށČ×īEg‹ú¯ÎCxõ–BęՐ ÕcqȈ, ԘaC°tŅB|øđ!|ܒ8;ãŋÍî(QŠ.kĶ)[´ãQã’$‰3núęß9tqÎú5Ģņáũûpč5÷Ŋ‘;o>!|ÔŖ<ŧ.…ŦGØ›‡ėÕ+–˛E›(Iŧ}ķu—m“æ-1ûīÅ´ŦG įĖ‚'ÁÁėŠ"ĀK–<9ģ>˛RÕęÚ1ÔLKēwh÷͛ØĶ‰'aQ øņãŖ]įŽ5a˛™­ę÷1Â͍—wԠׇ˛”'[$W6„†ū_ČŌĩØëŨwjtĒņ›EQ؁ŊÃŗ§OŋrN’$6zœîŖą$(*ûcŪxo$de4–ž¨ũ¯;¤Ĩ#™؏Û7o‚ēƒˆĀĀäΝ7oŪ@Ú´éā’6-Ō°?Ķ!gîܨ\­zxb¸ÚƒfĢö(*›/K|üø1ŧKAįŊoé>kpˆˆˇĪ¯]ņúÕĢp3düįŽß´ĖVíĮđ:Ė8 DĸŨëæ=$O‘ÂĒ}kąqÂĶĸũ¤ÕčĀ(1ĢG‡ļ,uÉđÅō€įᎊ5ŽĘŠ5Œ¯qTV¤hŦžĩ¯;Œõš~ {wy`˙Žxđā>ēté‚ĉ!]ētHŸ>}„?āīīßô÷ˇoąhŅ"d͖5ęÔE­ē?ëöĩƒqTV´hŦ˜*—.Ž›>ŪėŸ‰%”YsҏYs%ŸKē¨3jč`,_üOø+͍›`ÁŌē°]m#yOížUjī6€Zß­JMÚ´‡ČĘwnßBŨĘđúõ+¸5k9‹ūĩ)øļčĖ•Ĩ/Ī"Ec ØGeõEëđđēđBvŅü9liüxņáÚĀ \]Qē´ųšK§NÂ6wwlÛē Ÿŋ|F›ŽŅŠûoļāÕú0ŽĘŠ5€tæÔI´iڈEe“%Kī‡UÃO+ ũ" č vĘÉÛ}ä˛įČŠĶljáŲÔ0åŨP Z=‡˛4`nõjáĖIOÜô DÂD‰ô8†&m.U(/ž=y‚ÛūOLÖÕcŠĘn\ģģĶeJ4­ÃÃë ŲuĢWb¨hÕĒ~īÕ Ų˛eS}mŨģw3gÍÂēuë0tÔX4mŅJõ>ŦÕ EeW,Y„†Mš “Ģ&õjãô)Oü9vēüÚĶZPÚ­ŨŲͧb攉øąD)lôØm7;ėŨ1áŲÛÖhú—BVŖŖÔ,ēœ„Efë7Púˆîę]žtīBŪĸôOåtgģƒé5ŧįņcpmäϤēfęØL똅uëm§uxx]8!ģĮc°?•-‹)S&ÃÅÅ%Ę wüøqœ\]]ҰaCv^ q!QL‚6iʔ˜6G;ˇI‰°hM} ˆîŖčū™ߘ~ĪCx–ôcÅgo¨@¯ģ…˛‡öīÅž];Ų> —4.hßžœMî¯xųę–/[ާOŸĸzē,ČQšPĄŊ"ē"ųįZ‡‡×…˛īŪže‘Šysį NJ5‹X&Mš„Ŋ{÷"EŠL„Ōũˇmßž áįΟŗž ô]3îÛˇŖ˙Øwâ4;ÉE¤E~ĸû(ēj­ ÂSĢO•Û‘BVe@ÕjŽnœ3} 2gƌ ž~vd˘‘ģųGąĪŽmÛÜņđŅ#ôôhƒĻŠč>ŠäŸ#i^×Ŋ=ö ~q­‹Ë—/#GŽxáŸūÁāÁƒŅŊ{wLœ8QuΠļ/^ŒÉ“'ŖsįÎÚŋyķ& .Œ­{ ˆ ¯)iŅF7`ĸû(ēj/DÂSģo•Úķ@ɕ$hõX„‹Č>°FŽ@Á0}ú4dȐAĩq!Q;`Ā@øÜ¸ĄŖĮ˛ MėQD÷Q4˙MëđđēŽ…ėáũûØf—ͧNFāĢW¯â—_~Aųōå™ČLž<šÕxâŲŗgL,Ÿ={kÖŦAž|o)Qĸ$˙9å+ĶUęÖ+ĸ-Ú¨ŨGŅũŗÖėį!{ö”]˙üßęÕ¨\š˛Z͚lįāÁƒh׎=ö{žÆoÍ"ē"úį¨Z‡‡×u)d)Ä>vØßEb˙øãĐkژe¯Ōŧys–â0~üø&PdvÜô™ĒĻˆ¸h#›č>ŠîŸ-Ö!áŲÂ3ú¸NG‘øzƒ‡ūŠî#˛žĮŽĸ[ģV,EMÍ4ĨCIŸ[… ²ĩ­vŧ•č>ŠčŸ#k^םĨdįB92#$$$G´mۖD@¯ųí]&L˜ŠĐŌM`ƅ6~Ũô ]'jiqŅFÆDtE÷ĪŌ9ŽôyÂSÚĻë]Đ Z=] ŲģˇoaĀoŨqō¤§Ũą/UĒ4f˙ŗY˛gWÕŅ}Ņ?G×:<ŧŽ;![ŽX!ėÛģ'ÂÆ.ąE‹Eŋ~ũT]ü–46uęTvFí’%K›aĮwš6Ā‘ŗ-i".ÚȀˆîŖčūY4Á9æ!<ÎĻmU] Y[!Šú29mÜhMˆXƒi$f‡€ReŖ>ˇœ*Ņ}Õ?G×:<ŧŽ+!ÛīˇîhÛ˛y„#ļ(€ŽŌŌB$62ÁPzA;nܸđ_Ņõļ6mÁÔ9ķyųˆÕuŅƒ!ēĸûgÖÄļā!Âŗ k>z%֓ ÕcŅeD–Ōz*•(Š'O´wå*mP>}ÅĮâœYŅ}Õ?Šu^׍Ĩ[,ļŦ]mÛļ†=mėōđđ°kNŦŠOÚøåææ†f͚…W­[īg´hßÕk}æmLí‰ēh}ŨGŅũ3ĩŦņ{ÂŗF˙*´y@ $hõXt)d‹įˉsgĪÚ%'ÖÔ ?|øåËWĀéĢ–d!ē"ú'ĩÎ×ÕÁÃë瞕JÃv÷má7vŅ[t3ו+ô íBWÜnÚ´)ühŽk׎Ąiŗf8pō—á".ÚȈîŖčūqMh•*ķžJ]ĒŨŒ˛j#jĸŊ?úôB‡ļ­mz:¯‹ĀĒĩë1aúLŪGY}Ņ}Õ?Šu˛ëV¯„×é“Xļliø‚Ļ];wî´é[fą €;wî qãÆ¸téRx­[ˇA™JUāö bLQ­ąįĸû(ēĻgąuj d/h@ûßĘŖB]Edéŧæe įcĪžŨ֙*ļZ­ZutëŨ8Ī!ŨGQũ“Zį˙‹‡‡×u‘-’++Ž^šæ%ŨØuūüyüũ÷ß*R†u›ęÔŠʕ+‡Ž;˛Ž?~Œ%Kâŧ÷m“‹ēhŨGŅũ39‰­X‡đŦh†%MĶ7Ü6HĐęąčJČÖ,_ĢVްęej "}ÎuîŌ;įjRtEõĪŅĩŽņ$įáuÍ ŲEķįāyPūš1#ÜGÚÜEĮ[YķÆ..ÖPP™6Đ­_ÁÁÁáĩ{õúé3gE‡n=blAÔEkė´č>ŠîŸ‚%`ĩ*<„g5#,kX YËđSü4]˙|tßŦYķŸâgė]ąiĶf¨ņsÔoÔX‘)ĸû(ĒRëDœŪ<ŧŽy![ĄxvÜVļlؘ—“&MÂ˗/Ųí]z+DÚ´i1`Āfú­[ˇØq\‡NŸÖQ­ąÃĸû(ēö^‡<„go[Ŗéß @;˙Ī=Ō¨ĄŅ˜Ĩ›ˆlĩ˛%ąß^MnđŠnČiãWŊúõą÷Ø)EŗBtEõĪŅĩNäÉÍÃ뚲>ׯĄWį¸zõkęXXXĒUĢēŌO…ė¯Zĩ*:n~Ū|ų°håäĖ'J—D]´ÆÎŠîŖčūŲ{-ōžŊm•BÖ~#ph˙^Ŧøg!vīŪe?#ĖėšzõčŌĢ*š¸æ\tEõOjī¯kZČΞ>NŸB1áÛu¯›7ofGmŅ z- 4åËēēē2 Œx‰“âˇ>ß_æ ęĸ5;Ņ}Ũ?-ŦCÂĶ‚ŊQØ@7¤t@‘Y=]Dd‡öīƒR?E1§riqæĖ™ƒK×|0vĘ´ÍŨGQũst­Õ¤æáuM Y×ę•1wÎl”.]šųŲž}{TŠRíÚŅ[8}–˙ũ'OžÄâŋ™'Nœ@˙ƒ°e÷ūīuŅ;*ēĸû§…UČCxZ°7 .čž+˙ėįœ.„l‰üšpæôid˘Ņ~H™ŲķƒPąb%œēâc ĸû(ĒŽŽu„˛O‚ƒPí§R ÷1eʔė(+ÚėĨ×B›Ŋčč°   pRĨJ…c.#EŠ”ÜuŅ;)ēĸû§…u(…ŦŨGAķBöĘ%/ ūũ7\ŧHßôY *ŒY˙ü‹| Fé€č>ŠęŸÔ:Q¯G^×lDvãÚ˙đđÆģŪõøņã2dŽ;ĻO2˛ēlŲ˛øë¯ŋPĻLöŋ ‚Ė9ķ QSēĨōkuŅžč>ŠîŸV"áiÅæHvĐnĪ.ôǞ4/d˙™7ÉÅGīŪŊ5:L›5mútŧ˙ tęūk”•E÷QT˙]ëD7ķyx]ŗBvԘQH›2†Ęüœ:u*'NŒ_z›ĻíÔ |§?ĸ_ŋ¯yącƌÁˡ0`čđp#E]´ÆŖ ēĸû§•ÅCxZą9’tÅ_7Ņ_ĸQÃŋ™Ĩy!; gTĢ\1üomÃĩu‹-ÂQĪS˜2kn”D÷QT˙]ë-d#OZēH B… čЁöDčģP~ėéͧAÄD%*‚uŅœč>ŠîŸVVĄ˛v Í ŲļMĄ_ŸŪ¨S§ŽŨÁ2׀;v`îüXēvc”MˆîŖ¨ū9ēÖZČFž´uëÖE¯^ŊtMD†ķđđĀ‚ @ÄD…ū={î<,[÷˙ĶD]´Æ“VtE÷ĪÜdĩŸ@Ȟ@[é)2ĢĮĸy![ģâOXļt Š+Ž/ŊŖũ GEŅĸEŲ˙Ķ­‘ŨēuÃîŨģQĢV-ö6l`oΜ9ŖxlĻM›ŸđMŊŠŒĄâ… ĐąSgė:r"ĘZĸû(ĒŽŽu„˛‘'-ĐŌĨKà ‡œ÷ķķc9O§OŸØÍYŖF‰^s m&9r$V­Z…sįÎĄyķæ¸}Ûô5˛QõGyŊŨģwĮÕĢW#üšŠK—.ėš]*Q”¨‹ÖŅ}Ũ?sטÚĪ dI!ũ€­‹æ…lą<Ųáuņ"ŌĨK߯ŖbŊčͧû˙úõëÃÉÉ ™2eÂŧyķØ˙‡§OŸž}.(-Ö˛ūūūøąxq\đšĨĸû(ĒŽŽu„˛‘'-ÉÅHDD¯‰ōäÉÃnųŠ?>öė؃ĻM›˛zšråRĘ9ęíßŋÇĮŠS§đūũ{<~üŲŗg7Ģ­č„,RņâÅYÛTĸ"(Q­1ĸû(ēf- +<$…Ŧ@åkRķBļYũÚ8väČw^QZŊÛēu+ŪŊ{‡ŦYŗ˛ kj׎ ēQ‹ }–ŦYŗ%J”Ÿ8sæLĊ ... ũyķæe7NßSЂ¸6ô"˛$ˆ.\Č>ŸčfĮ?˙ü;wîdüäΟsįÎČ“$I4lØÕŖˇu†c'ŽPŠÖoßåčˆîŖ¨ū9ēÖZČFž´t~ŦņXä|ɒ%Yōžņ×ÛˇoGá…‘%K&hûöí‹W¯^ąoŲtęA“&MX¤•^‰°Ŗ°f˘ÁrpéĶŖGP¯^=v•Ŧ!"Kß°éÕ‰ÛįΟ3Zģv-~øá‡hû‰NȒí‘ũ‰LPĸ.ZãI+ēĸûĮ§uŦW[!{@/Ęß][NsZÖŧîKĨ¯¯/Š):‘-ŊõŖ‹wHœ’xĨԃRĨJą`ųŨēukļŋ)ŊĩŖēׯ_}>PZÂå˗7n\&pIČ,XëÖ­cm“X={6{ˇdÉĎSĻLaâwŊH ĻOŸŽŽ]ģ˛ß‘X6.æFdEņQÔ1tt­#´Uō-…Űų‹DhųōåA7g%MšoßžEĄB…āîîÎÎm}ōä û†K߄_ŋ~͎žērå KG ųûīŋQGdS ˆ¨¨U„ ™€Ļ(ņ°aÃĸí‡Č1ĒÔK"˛ĸM^(šT^ IDATQ‰É°0E÷ĪÅcg˛§ĐšP$hõX4/dŖJķ1MŸ+WŽÄüųķŲįq;1Ō¤IÃ+åВđ¤HjHH›†BB—Ûļmc‚–>K¨ĐįÕ d˜"­TjÖŦ‰[ˇn!Y˛dėߔGŸ'gĪžeB–Úˆî  99˛"ų(ę:ēÖZČ*É1@ß|é|؃˛¨íž}û˜X­V­K=0”/^`ōäÉȑ#‹ĖŪŋŸũŠk›6māííŖĨoÎ˖- '*ʧĨįĸë‡nQ3GV$R"_D%&Ã8‰îŸV—˛v Í Ų˜6^4ˆŨöEē(ZJâ•>Gč-]žCÁ777–rFéÆB–ĸŦ‡f 2Pš€AČnܸƒf›”)hBmŅgE˖-Ų5åT(āōæÍ–Ļ@B–ÚH:u”jÉŠ"ø(ę:ēÖZȚÚÉGQMJЧ×;ô*ĮPzöėÉŌš5kÆƒŌ …žĄoĐUĨWD$€ BÖđī˜"˛´é‹r DE˙nÕĒU´ũP*BTBÖŌS D %ÂPTb2Ė7Ņũŗģ|úf€Bö$€ž(2ĢĮĸy!ÛŋgTæY­ôYBû, 'P¤”rcé?EBé-Ef)pAuHėū÷ß .Ļ€E`# YCŽ,mėĨĀ ĨĸQ*ĨøuvvfįĸSĒĨ1˜˛ĻΑŨGQũst­#´5uļ å¸Ō tą}Ŗžqã–”3ÛĸE äĖ™“}ĢĻ˙#ŅIÉú'Ožd¯‡ĸ˛D0Dj^^^N- ĸŠJČŌˇøčúyúôi”B–‰^%QN•¨JÔEk @¯ßŌu!d‡苒E Gx̧…ųĢÄJa¸â}c&Oąēč>ŠčŸÔ:QOi^w2ąˆ˛ S¤ŗ*Yl ępžk*˜3k&;… ĩUŠR%´oß^AWÚŦBųPtlåÅRĄôƒƒ˙Āæ]ûž3XÄEŲIŅ}Ũ?-Ŧ2ÂĶ‚ŊQØ …Ŧ æđ}Xļp>öė‰úB˜`vÕĒUGˇŪũPĄr•ÛŨGQũst­Õ¤æáuM Ų93Ļ",ô=&N˜Āüܲe ;īÎåĶkquue×ĶŌUˆT„ÎÉņkī¯ų˛ÆEÔEëH>:ÂÚ{-ōžŊmĻ˙c†Đ÷ZÚgĘ,Ž…ŠÆŦųûĒeŠcīž=ēJ/ Ŋ ?ģēbßqeĮ ‹îŖˆū9ēÖZČŪôņFömpũú×ŧ ēM‹nÄĸ“ôXhÝ#h|CYîŠčŸŖkČŗ›‡×5/d˙]8Á~ž˜5kf¸Ÿ´Ų‹D m’ŌK bg Ōf/CųíˇžČ”='ÚuéŖ".ÚČ‹îŖčūŲsōž=팡oڇ0ŸZŅŽ3ē‰ČdGĀâšŗąoß^í %UĒTÅoũĄ\ÅJ\ļŠîŖˆūI­qŠķđēæ…,šų.âČϏV¸*GŪ¨F‡g—)Sg¯ß4i‘ˆ‹6˛Ķĸû(ē&'ą+đžͰ¤i)d-AΌg‡ôīļ-šŖjÕĒfūŒŅŖ)ØōĩlŪ˛[ˇīĀä™sÍęJ´Eĸû(ēfMl @Č0 Z=]Fd @ßŋwŊģt™3öŗ$b,[‰LYÔēƒčĢ—ĸû(šRë‘5NÅEáąc;čuŊĄĐôīéērm”‰'âÁƒX¸pa¸A”áÖ¤ žúŽŦ9֊ļhŖÂ@tE÷Μymî3ŲÆ AĢĮĸk!K€Ÿ9é‰vŋ¸áōåËȜ9ŗÍĮ€rbéT›5[w¨‰ė„č>ŠæŸŖk^×MjaQ~øđy3ĨÃû÷ī#ŦĶŽ;"GŽ ­Ŋ Eb?~AĒMqâÄÁũāˆ;ļÅ&ŠļhŖDtE÷ĪâIްÂSؤ­ĢI!kkÄŖčŌ~j”+ĨK—°‹klUčt‚ŽŨēã€į‹sbMŲ,ē"ųįčZ‡‡×u'diĄ^ēxÃû÷Ášs/>|8;_–rgíU('ļ@Ō Č–ĸE‹aęÜ(X¸ˆjω´hŖEtE÷OĩÉCC<„g {Ėčc?€‰HĐęąč>"k ú°ū}đāîL™2ŋˇÚxĐeƒ FļÜš1~ĒyGl™kœč>ŠâŸ#k^×ĨĨÅ{üđ!Ŧ^˛[ˇn‰°–ׯ_QŖFaûöí,BkĢBē^Ŋz ”ã]Ôŋkƒh×Ĩ~â<ÔZŠíĸ,ژüŨGŅũS:—ÍŠĮCxæ´oƒgö Ģ IĐęą%diŽ>„‰ŖF gŽ˜>}šĒé÷īßG˙pīū 5–û˛ĩ&ˆč>ŠâŸŖj^×­edsč όõ]d–Žæúå—_P˛dIv•męÔŠÕZûßĩC7v <˜Šĩf͚đ#ļ );bÜD̉XC?ĸ,ژJtE÷ĪZ‹‡đŦeƒ…íJ!k!€Öz|û–͘;c \Ō¸ AW¸ēē"K–,ÜŨQ,WļmsG`pú ‚z rˇcD÷Q˙QëđđēŽ…,-j Ŋ7¨Y•%éįɓ'Â:_˛d ™íÛˇĮ”)SāädĘ]å4ņåËļšlÕĒUL,Sƅ6v.\ģW5Ā”…",ZG÷ŅÆĐÔķüž‡đxÚĩa]ē+u*´z,ÂEd#ÂŅCąo×NėÛ偤I“ĸ}ûvėĪôéĶ#C† ėOúņ÷÷g?´G‚ū|ņō%–/[Ž7!!¨Qģ.jÔŠ‹ •ĢhrŒE÷Qīū9šÖááuSʎžzŌõ‰jb eIúĶĻM…kũúߑĴiĶØ…D<ôš~Ō¤IÃM&ė[ĩģģ;^ŋ~úõëŖ_ŋ~ßĩŗiķfPž.]x ÆÆ.nCŲ}â’xõūáâchÎ܎ü áŠŅŸÚØ`:´z,Váu­á}í*<Å īë @€ŋ?=|€”ŠSãÍë×H›6ŌĨO4iĶ!wž|(_ą2ōæ/ Uwĸ´+*>¸–&mZŧzųŌIī>F域ŸoX„ ņņãGÍúįHZ‡‡×…˛†•8đ÷ßđ,8S§LfŽ" †ƟmŲ˛%hâOdácøfM'$P䕎Ķ"ņJB˜ūŒ\(Ŋ`ā ÁpIŸŪėsb­ÁvQ-ÚĀĀķa”‘.]z'‰7 Ā...Öũ‡KtcHãöØĪéĶgĐũ‡§šsŸ‡đĖíÃĘĪI!ke€­ŨüÆĩ˙ąāÁėŋ[ģ+ģĩŋ`ÎŦ° Œ?ɔn°›–t|ŲëbØĀŪ=ąįČ ÍûįZ‡‡×M ˜."˛Æ“w˙ž],I˙Įb?˛]§ôÚ'ĒręÔ)xzz˛Ģn ¯ƒčOJO¸qãF¸¸ĨįéZÜråĘĄTŠRQļåëëËvŸ^žrCFåžąË’ÅgÉŗ™R:‡Ũ zNĮ‚™š–tcˇgCŪŧAŅ<ŲqË/Čn6XģãڕĘaĘĖ9(\ėGkwĨŲöyOŖNė@ÛÖIĐęą8TD6ĒÚļy#vowĮ‚Ĩ+ô8~Šl–BVL6Ģ$ēÖááuSFwBÖ0‹6­[ƒ ŖFĀ­ą~˙Ŋ×w›°Ô˜mtRÁĖ™ŗ°mÛ6 =šūĸFŗ6kCt!ûüų3”/V×îûÚ S[wTí§R˜ģh ō(hëŽ5ĶáiÆčˆ†ė0 Z=‡˛ģļģcãē˙đī*í^—néĒBÖR­ķŧ¨Z‡‡×…˛†)ŗôīXądû'ĨĐõļ]5ˇ?~ÛÜŨ™x/Ú´ī„v]ē™Ûœ]Ÿ]Č f…ŸāuķŽ]qļfįJÅŌ5ë‘3×˙oēŗfZl›‡đ´h?)d5:0JÍÚˇ{'V-ũË×mRúˆîęI!Ģí!MëđđēđBÖ0õnßŧ=;=°÷NÜēáƒ.]ģ!QÂßí:5Ūyjŧû4äí[,úįäɗÕk×E­z?#GÎ\ڞŲ&Ŧ]Čú=z„†uĒãėÕē§˜Œ/S$?ÖoۉĖYÕڏŠ?¨xOŖŪí0į› Õ¨‰1šåđŲ#÷cáœŲXŗÅ]ã§Čf)dÁd÷Jĸh^w!k<ģč•ķáũûpįÖ-PԎØ@ ræĘ…ÛˇnąÍAiĶĨƒKēôpI›9sįFåj5}ŠRķ'guãlÔáŲČ$Ūn¤åELcõĪž>…q#†bÛۃŗL=s¤UKŲ’ixx] YĶx [Ct!{õō%ôû­;ö;)ėæÍœgŽø i˛dÂúhĘ1Â3Ֆ~ŋĀ$hõX>"K‡Õ˙Ņ÷wvލE YQGV›~ņđē˛ÚC›X%ēŊxū†ėƒGm‚§=:ɞ.ŽŨķeį!;já!<b´ĀßHĐęą8ŧŊvå2úôčŠ}ĮOéqüŲ,…Ŧ"˜d%•āáu)dU]Íˆ.dΜôĄŅbëîũzE6gJéŒûÁ/ėvƒœ"#­\‰‡đŦlŠšÍK!k.ryŽ6wnĶGÎ\ЈEę›!…Ŧú˜ĘŖG€‡×Ĩuā™$ē=~ä0fOŸ‚õîzŨCķäüōå 2§J ßįoxƒn6 ĢX˛čķˇ!!Št mu§3IĐęą8|DöūŊģhŲČž^Wõ8~Šl–BVL˛’Jđđē˛*ŽĮfD˛‡öīÅâķązĶV=I›éęäüY2ānā3“uEŽĀCxÅA YŒRŗü|}Ņ VUœģvSé#ēĢ'…Ŧî†L×ķđē˛ējˌ]ČŌšÁkV.Ã˛5,JŖOŋ~õ % äÆGĩĐ6fņžm,âîe€čõR‡Čĸzš2¸të÷āëå)dõ2RbØÉÃëRȊ1æfy!ēõØļ[7­Įĸ˙™…Özöė)*/Šk÷iŨTĢÚĮCxV5ÄüÆé•ÁR$hõX^Č:ÂuØRČęqię×f^—BVŋãląåĸ Ų­×cīŽ˜˙ī2‹ąŌbū¨]Š.Ū÷ ^%¸ķž’öėPG Y;€Žf—!oŪ hžė¸å¤fŗšjK YM ‡đÆđđē˛ÂO‡č]ČŽ˙očÆ™ ūr”}>Dãz5Ų9˛Ž\xOŖ8m°€^“š>"ŠÜ]Ø "ĸ)dEYmúÅÃëRČjs mb•čBvõ˛%đēxSgÍĩ žļîäîÛhÛ´1Ž_¸lëŽ5ÕáiĘđ˙ŗĀJ$hõX^ČŌ eH–_žÕãø)˛Y YE0ÉJ*!ĀÃëRČĒē›]Č.]ô7nßđÁøiéqxLÚ|Ãû:ēwh‹C§Î™Ŧ+rÂĶ(RČjt`xĖĸŖđč‘8qâđ<Ļ›ēRČęf¨„0”‡×ĨbČÍsBt!ûĪŧ9xėį‹Q&›ÆŸērÉ ũ{ũŠŊG=5nŠuÍã!<ëZbvë›Ŧ@‚VEFdäHŸWīcÆæ/_ŧø1y˛dņŪŧy“ÄŌ6?|ø/v먟âĉķÅŌļŌ¤Mû(0 ~ܸqĪ?öl>€Ŗ–ļŠāy)dΙOžAę4. Ķ_)dõ7fzļ˜‡×ĨÕķH[h{d!''SSÂÂN­üøģˇoÃ_íM7ņâĮCŸXšWÛ5O— 8'MĘ:׎ô}ô¨•ú=EhŅ!…,ģuÃztčڝQ".¸ī;Œ 3bÜČáøŠ|ET­QĶĘĐ[¯ų?bŪĖ(_Š2J”* c!ë}í*öīŲÎŨÕu*ÅĻuk@W}7mŅ —Ŋ.† ėŨ{Žœpzķú5Ν…vē KZë,[Ž^7ĨZ˛ ÷–YUÂÛ! O%ė,næÔ‰ãčßŗ&Θ‰ŠUĒÁ dO{žpÔ§&N§˙¯jq?öl J™([žÆLšĘĸ•)R¤Äo}úaڄqđēpĢ6ęõ„Ŗ¯¨ūҝ7Ž^ōÂŦŋãÁŊģXō÷BŦÚ¸[6ŦÃ˛Ecá˛UHŸ!ƒ=‡Āæ}ķž™Æ%Ȟ#įíåk7fĖ‘;ˇ™MØįą-ÖŋYŧpŪŠ‹įÎÖ°ĸÉë¯^žDÁė™@€ŋ÷ÁļÍŅĒm˚>…‰Ŗĩ[ļŖôOåŦģõ›nßŧ)Ž:€ŧų āĮ%ÃBB^#8(ČéôIOd˞ûŽŸ˛žVėaÚÕÔģ'’&OŽŽ]ģ‡íØļ%K—qĸoh\>}eÅŪeĶ1!ĀÃëRČ:Ø\ú!EŅK—>núx‡åÍ_ĀÉĪ÷(Ō÷čŲkĊK׈Œ6K˙ĄĄ(ZŧâÆ z/^„~ĀŖ÷ņîŨ;öÁrđäY]ûgŽņ<„gNû~øáܞŖžÅSĨJmÎãvfë† o&ųsÛÇZ[ɇ˛„åŸ Ċ%‹Ų›,â˜ĪŸ?ãmHōæ/ ÄZ¤KWĘ-ˆ÷īŪąČkėØąAŅĘĉ“`ķÎ=(T´˜•Ļ”íš-’+čŠá„ "nÜx ũĀžˆô4Ŋļ!˛§đđē˛6yĻŽ‹yŗf ôÃ$HīßŋCüøņŅąÛ¯1vŧîŅ ÚtA¯Åˆ”čUë‡؇Œ(‡•W(QwnŨdc•(qböÁ)Ō ī$ä!<Ūļ“ĨH1¸GĪ>#00!īŗZĒßö—&ûwīlā„ėrX!KQģŒÉG€4‰ŗ3ģM°z­:V€ÚöMūÚŠ=č–DãRĸT¸ī;h{cŦĐ#ųF§ŋPZšĄˆôyaČlŌ$¯K!k“!ŅV'™R:ŗČĄP4A„hŦÁŸŪŨģ€^Jâ$I0lô8´īÜU[aĻ5Ûļâ÷nXÖPĒÕŦ•ôzz“™@|{Œ‡đx{J–,Ų}÷}‡˛äʓ—÷QMÕ§WĨFXõüųķ6V0Ėa…,ai §ĪįŊoYfû4i•% DüŌlˆĘ’$be4Ö>s͸W^—BÖūães Œ‰W¤hŦH˙ĮQūĮÂx÷îë7ėä)Rāú}?›ãlÍi‡ô“ā¯÷ē“Pßü4˛dËfÍ.5Û6áq:‘0Nœ8¯>}Ĩûî/_ŧ€ĻŽuož~õ*'JĒ;´%€˛š¤`o~čõûô9ķҰI3%¸éĻŽqTV¤hŦaŒŖ˛2ĢiÉÃëRČjcĖln…!*+Z4Öd§ÖÍąkģ;û`™6{5ũÅæ[ŗÃ5+—cÄā,­ Ą[SĖ_˛ÜšŨiēmÂãt$sŪ|ö5ĒS›<öčū¤‚čÜ%RjÜŦ9*TŽĸ}U[ŧ`^Ŋz‰~ƒ‡ęËp•­å!<ÎŽĨU—õ{ôķfNĮí[7pīÎe=ČZ‰iŌ¤Aü QļBE ö'×ŗZ¨ĖÃëRČ*1Iŧ @’U$"`.ņō§‰RČ*LąŊuí7ĀÔŠSP¨P!$K–LY˛–D@"Ā…•v÷î]öĶĩkW\ģīĮŽĶKááu)dMŒĒ$^ŊL{i§Ū0—xy#)d•ĻHČ^Ŋ| Ŋ:w€ˇ˛Ve-‰€D@žF­ē?cĀĐáprr‚!"ģmĶœ8ząbł“ØīfĖ˙÷îÜA×ļ-Y_ē˙†ßúô‹€ĨŒČęoqmŨēM›6EĒTŠÂ§yU­Z5,Z´I’$QÕŠqãÆ! sįÎÅļmÛpũúu 2$Ę>ČļŠS§ÂĮĮ‡}š*WŽƌƒ‚ Ēj“lL=¤uĀsdej˙’Äˏ™|"z”/Ī7wNŧcŒČŪŊsqãÆEč‡ˆ7.*U­†žƒ† ÁBøwá|\öēˆķ2ą:oæ ÜđžŽŲ/Fë&P¤Ø8lž?×UąvËvü9d ĒÕ¨…–í:€„q—ļ-™˜mŅĻ]!ģrÉbđ<ƒ„‰aú¤ņxėį‡ésæCFd9GWãՉO‡ŽĢW¯†[úüųsT¯^ŋüō  ¤ĒÆB–úĨčū¤I“žëƒDôØącņīŋ˙ĸjÕĒŦŪ’%K˜č=räŠ)ĸĒ]˛1uPʧęôfy+<ŧ.#˛2"kųŒûւ$^Õ ” PJŧ<„Į ŦĸԂ÷îĄuĶFLpūÚģ/ëĸyÃú¸w÷6’&MÆūMö $ĀÎCĮÍ%<ŊŽEČwĨH[ÎôŠ‘5{Žđ´„woßĸJš;yZ!ā˙Ŗ'Naí^šä…-šâÜõ[RČrŽÖĢGŧwīŪEíÚĩҝ_?tīŪÁÁÁčŲŗ'nŨē…ĪŸ?ŖAƒ=z4‹Ô:;vė`_ļŌ§Oe˖!uęÔ,‚J‘Wú;ú÷“'OX$–ūŋuëÖhŌ¤ ‹î÷îŨ;‚` EƌązõjÔŦY3„$dItoßž‰m˛ÚĄB˙&;6lˆ‹/ĸoßžxõęŗ“žŖzÆBšžĄ“]3gΌŅO­ŖVėSʧZą—‡×Ĩ•BVĩy+‰W¯j“IGB–|ž|ņęר‚õî;Qú§rhæZš4C‹ļí$$JCBŪ Uę4L”?é3d`ŋŖƒTŠRŖpÎ,8|æ˛fËÎūŸĸĩqãÄEgįBÖ˙ąÆLšĘę\8wŨ;´Á™+>RČĒ9ų4ĐņŠ››œ™HũđáōæÍ‹-Z0qI‘ūƍŖnŨēčÜš3‹â“ $1[ĨJ”-[žžžL¨’(,Y˛$jÕĒeRȒ ."{åĘ+V oßžEŧxņ" tėØ1&TŸ>}­%ņ[¨P!¸ģģŖ@L¨–.];wîĆ ÂS" ŲčüėØąŖFJ&H!+S ô1SílĨ$Ū˙ YIŧ–OFĨÄËķ͝Ķ*EYC›ū1ĮÂŪc'ņĪŧ9p߲ ›vėfBtHŋŪxņâ,YŽö͛ hņč3đ&Vk”+UˇbܟÃY”vōĖ9x˙îÕŠVí;ĸM‡N„ėŌE ąį¨'’'O~ŋuGĸĉ1~ę LŸ8/_žšÆžĘYΑ×@uãĀ Ų)SĻ`ųōå8xđ 2dČĀō°'NŒœ9s†GņI`ÖŠSͧOGŊÂ"¸$v+UĒÄŧ2‘U"dŠŨøņŋžØa({öėAķæÍaHˆ*"ëââÂr|ķäÉū­‹É“'ŗ¨˛!G×XČΘ1#Z?g͚Ĩ‘Ō‡ JųT+ŪđđēŒČF3j2G–:Kâũ*d%ņōĪ¨žPJŧ<„Įi—}õō%Ę/‚n=G×ßzaԘ‘Øˇ{ۘ•#W.L›3Ÿmö đ÷ĮūŊņčÁ&F¨nķÖmŲ˙ÔˇoŪd¸Úõęcč¨1ßmöÚíą Ãŗ§OQäĮâ˜4cËgNzĸC‹f¨ãÚĶfĪ‹āǞœ#¯ęQŊájÔ¨Ūŧyƒ}ûöą9’(Q"ļ)+GŽĖâgĪžąTŠâRjĀéͧqāĀŦZĩ ôė„ Øī?~Œ4iŌ°”ŠŦŌs†Ô‚˜„Ŧ!ĩ`ŊL0Sšv틎0€mūĸt†5j k׎lŗ• * ˙ūlãZ˖-ņčŅŖp„ũũũŲ˙Ķæ1˛kŪŧ¯swĈxũú5å1ųЁĄŌ… JųT+Îđđē˛RČĒ6o%ņJâUm2i<ĩ@M?yÚZ0{&îßŊÃĸļĨTJ/øįŸ˜ ¤HkĻL™°páBŧ{÷ŽEaģté‚%J S§Nđôôd"ÄéîŨģ™Č¤h.mØĒW¯Hļk׎EQ…ė¨QŖØĘO\/^ĖōpiŗEy)ũruoܸÃ‡ŖhŅĸĖ6ĘĨ%qęííÍėĄŧZŠÆR™žėˇjÕ ˇoßfŋ;yō$(568q‚‰õʕ+ŗöɆčüėÖ­›6KVH!+S t0Míoĸ$^IŧjÎBĨÄËķ͝Ķ>ÍŨė%…,įę¸zT|JîĐIôJ„# ž^ŊząH(Eh)Guâĉ,Š?lØ0lܸ‘EgIĖ’ØÍŸ??ËEĨ[ú?WWW&0/_žAČ?~œåÚRЉŪȅlŖt˛R(âJ"›„'åãRš@ÛļmŲ†ŽÂ… ŗČo‡˜}§NbŅYŠļŌ ڔF—ūMâÖËË ™3gfšŗä YŠÔFį§Ž‡ØĻĻ+åS›Cg<ŧ.#˛2"ĢÚŧ•Ä+‰WĩÉ$#˛jB ‘UNŲXXŨĩkę×W˙f; ¸åH!+#˛–Ī"Ų‚æÄĢš!‰`RâåųæÎéąæ"˛œö‡W—BÖ\ääs1PʧZņ–‡×eDVFdĩ2oĨ)d­48„lB:-ŒĶŒLÉS¤đž~ß/qtĪÉÍŗœˆĘꕐBVFdUžR˛9‰€DĀJ‰—į›{¤>/H@Y .¸é'SŌdÉŧ|ú'7eĢÖIČ&@ÛÜs~û3?€Â˛Ø 5§?RČr&ĢKl€R>ĩĩ]ŅõĮÃë2"+#˛Z™ˇŌ‰€­#˛Ü@ŽJ(€7tÔ&gc“&Köģ`BvēĨĸŽäī×띞–Wž^EÆW¤åÃK֖Ø)deDÖæ“Nv(pt”/Ī7÷(0Í €.ŗüZü€ēI“%;/˜MāIūžÂĖ9gRČžyķ&Ŧfų2t‹™Šā‰™&ČĮ$˜đōō ëŨˇŸĶÚmēŠ‡×M‘JGā€BV¯.æē4R`”/áE‚‹.‹o €ßĸ°Tå6€\DŪėE‘gƒx§ŋ°Ā:9§•I!Kí5ŠW +–- ?ŧŸŗY]" °ēãȉ“3yš­ØîQ^—B6†q‘ÄkģI+{’DF@)ņō€ŠßÄ+ ØŗßÄ[ãžõ@öoYȒ‹oĐæŽ§~@šĀŋ(ōMВ¨=Ē`f*˛;ļnÁÖu˙ÁÃc‡‚&e‰€D@MŌĻKĮŽĪvI›NÍf­Ö¯K!Ã0HâĩÚ• KL" ”x^)#ņzë›xĨčãķoF,ũ–7úĸ°F†eΘ)͉ŗWoü`ŌXWđķõ kX̚ŸŸīŖL‘L5ˆŲ~ūúöģTß-‰ÚÜÖļžŅ¸ŠHČŌŗÛ6cķšÕėûÜšŠiY$k!@GLŌģaõfwdÉFû9õQđz¸#RȚSIŧú˜ôŌJ10‡xŖ!ŧBßÄk ÁFâÕ? ¤(G6-€4‘~—,~üø÷‚žĮ×;ē׎\FŖ:5îŧyũšN*ˆ\>X CŋŖđMŗoÂ6ŗ‘¨=cTWąĨgvēoÒŋįÃ˙ąBCiŋ™,z@āķįĪaaaat“—)Ũ w„ˇŅÅÅW._†k#7ü>`rįͧ+ŸĨUy¸$ņĒ ¨šûŠØqâ VŦX6ęQvc æ¯á•1Šŧ~ū&^× tčJ\eœ@‚.BI—>ÍŊĮ°˙Ë%˙, cˇļĒdö`ĩ)ãĮŸŅãāҏų R \X0gVXP€?FŽŸ$ųTÄ֘ORČjl@¤9öAā—õÂzöí •ĢJâĩĪXĨ×gOŸ`ëĻØļiü|ĄZÍÚXŋfõËīŪYãâ‚DŲsæēzüü%ũ$—EBŨįÚ5´nÚčĐc?ßĒV |ü äș+Ĩ 4hÜõ5ÖŨĢL+a#LŗRČ 3”ēpD Y] “4ŌÚH!km„m×~ț7L¸nÛŧ׎\Aˇ&hāÖĨʔá™iqî"Å~<ļëđqŨå<~ė‡5ĒŪõķ}Dšą1Ĩ ˜ {,f YãFΜôd_l(¯6Ož|_#ĩÜ@ОØ)dí?Žd¯K!ëH3ÃÁ|•BV~Åë"([7m@ƌ?0áÚ°IS¤L•ZyO•e#‘PEČ7ęyėčˇHí&*ZŒEiIØ&qv–čÛ )díŧƒvËÃëRČ:č$qˇĨĩß(ßôņĪyMėėyĩÆæÂŗ"B÷Ŧē5Fë襃p߲‰ ÛRe~bgԒ¨M˜0ĄĐ jÍ9)dĩ6"bÛÃÃëRȊ=Ú;)dm;üīßg§ ĐĻ-::ËpÖköt„Šõ áYĪ ‡nŲĒBÖ؃ûö°3j)Â_ąrvF-‰Ú¸qéĻaYŦ‰€˛ÖDWļ^—BVÎaBÖúCāy}úô ;m€Ŧ-¯*å!<ë#â=ØLČŖģw×NvF-EjĢ׎ÃD-å\;9™úXsČ1˛Øi)d-†P6¯›ZņōÔāeUm! …ŦuÆãÅķįá§ ÜģsûÛY¯MQŦx ëthĸUÂŗ‹âwj!k ëŽíîáé?7hÄĸ´õ4yz(…Ŧ Á–]qŨØ(…Ŧœ0Â" …ŦzCûūũûđ[ļŧΟ O(SŽŧz˜Ų’˛f§Ūcv˛ÆŽPęû– qûõ8¯&ėlbY,C@ YËđ“Oķ!ĀÃëRČōa+kë)d-,:i€ŽË:r`ø-[•ĢÕ°ŧa[ā!<ģUŌ)–% į€sŠˆ;>N€ĪŸá˗Ā—ÄH™ö5ž%œB;ö;ĊũącŋEț[øøŪĀmwžũųYIĮ6ŽŖ)!kđũ˗/á/Úˇ7ü⅘n‰ŗ1nēęN Y] —îåáu)du?ÜԁčBÖŧšAWÒxĨ 5ė¨,ˇĻšŽhņžyˆ(~Ē€JH’Ė “dĀķ œHáōiˆ…ôY!}V ^ ūˇŸx ŋū›~>ŧ>ŧBßûķđōđä1āw÷žÄÁķ HŪ ><Â̧[p_ąuÖ̍I!kėnhhhøq^'ŽaQZׯnĐڗ2ë ‘å-K!k9†˛åđđē˛Ęq•5u†€˛Ęėđ}á›ļ*W¯ņ5úÚ¸‰ōėX“‡đŦ`fS¤H͝_üŒ.ŸQ Tä* ä,$ĄlU.O›^Ą¸qūB?|@ŧøkđęų:GUîMisš˛ÆŽŧ{û6üâ…ķgO‡_ŧPĄrĨū:d=)drØíæ4¯K!kˇa’[)dcFøäņcá‘×bÅK†įŊ&HĀÚCŖjû<„§RĮ…āœŧ/Ū…´BžQÁ51˛įš;™Ųv= îųû×ŋÁŋ÷ø‚yx˙f€`ŗÛäPWBÖØŊׯ^}ÔnŲ„Ģ—/ÃõÛĩeËWāGAđ'¤|€5æ¯K!ĢąÁ“樇€˛ßcyáÜŲđM[9rå ŧ&Kn…čĄzCcK<„gĄIyÜebĮ**“ĸlm A" ›Tņņ`?ĀspÄũ=âÄ،WmØ"§VˇBÖũgĪž˛3jIØŪžy3üâ…ReĘĒ8HúmJ YũŽ-įáu)dõ8ÂŌfEH!ûĻëW¯|MØŧŠS§ ß´å’6"ĩ^‰‡đĖö%UÚÕCC´’9 šŨŒÍ<îŦ†DIū@Čë)VîW!kŒQpP`øÅ ž„_ŧđc‰’V†RģÍK!ĢŨąŅ2^—BVÄāĀ>ų=z„Œ™21" Ų¤KŸŪ!ĐšsûVxä•n=bˇl5nŠĖYŗ į?á™á|%8Å:ˇąQĨ‘Ûų‘ĩŗ>Āëø-ŧ~N‡ü~°’5 Ycœü?ŋx!((đkNmÃÆ(\ėG+ÁŠf›Ö¯ƒBEŠaÄØņX8wvXP€?FŽŸä4cōėßŊ [÷@ŧxņ´c°´Dxx] Ya†]:Bõ—ûąŠ—,͈wø ūa=ûöĮÃû÷FŒÆÍšcĘŦ9Âåį닭×ŗ,īBBÂ#¯šōäÖgrŒ‡đ¸€HáŌŠĶÍE¯)É;×ŖšĒüĶá3ž|ĻoxūV°Mh!kŒ—īÇl}Ņb¯^ždĸļ~ŖÆ(P¨°`ĩ“$XgO›:õá§ōņéķ§° įÎ:…}ų‚&Í[â¯ųÛßHiđđē˛BNĮuĒg—ŽØŧ~-’&M†ÄI’āÕË€“čLÉçÎ#SēŦNœōôIpøi~~žhؤ;m P‘ĸâ8iÂÂS J’äŋ#kž‰č1^CI°Š­ēâ`ˇOxķ27€{ļųq‡˛ÆŽßŋw—åĶR^-]BëŽDmž|ųU†×žÍeN Ÿ>~DŧøņAöãĮ ˇ‚sŌ¤ö5Nö.,<ŧ.…Ŧ°ĶĀ1ŖÜļŌ…ōąãRĪĩ!­üOPŪŧ~~ڀ÷ĩká§ ”,]F˙xā!}úš6°i#NŸ<~Ú@ųJ•́H¨gxOãÉ7~fzÄUPWUüîŗ?ƛįU4Ūá…Ŧ1–7ŧ¯Ã}ķ&v¤eGĸÖĩQdɖMEČm۔!*KŊĘhŦmąwÔŪxx] YG%û9*ĢįhėŽ­[Øi{¯I(*["nÔŦSOšąĮÂÖMXîkš ŋ8ĐącĮÖ$žZ2Їđb´Û9ųbÔlŅ UŨ´äžul™?ô-Ži `‹ H!Ë "]LÂnÛŧ™˛deQZJAHÆ…ŗ%ÛU_8w6?ÆČ “l׊ėÉaāáu)dvšˆī8s˜ŋ`Adøá뚲Z+gN ?ëĩ@ĄBhč֔ ØD‰kÍTMÛÃCx1:’ÂåÚ Č<⟠Ā…C 𨇠ƒ+…Ŧ |ĩ›;oŪo—/¸!EŠ”´jŲŖW.yŽ°žé}A D``rį΃›7o°ˆ˛KÚ´H“6;Ĩá§ Q°0í!”E" <ŧ.…Ŧ:˜ËVėˆí>zđŧ¯]Ep`éæĘ•ˇnŨd¤›6]:Fēų BĨjՐ#g.ģXL¯éÕ"E^3fĘĖ„kCˇ&H™JüˇŲÖœ‡đb´!^üŒũ/’ˆwXÁw~{ÖĪ;Š—O*Š0.RČĒ"5áyėčˇË6Ą`‘ĸpmԘĨØâ˜ĢCû÷bߎØģË.i\Đž};8;;#}úôH—.]øŸđ÷÷‡áĪ—¯^aų˛åxúô)Ē׊‹šuęĸrĩ*!"›qTxx] YG%:÷ûėéSØģsöîôMb"Ũ4iŌ0˛5ū!Â5ū ²eËáË 5ëūŒuęÁÚĮVŨôņÆÖ_Ķč‰Ä+ũdȨæÆq¨æķ^ ŨäFōԇ0~m LŅĪŖūÂ0Ģ˙=ŧ~‘CŖĨUÄČM=tÛ˙×ŪY€U•mqüv!6ØŨŨc‚˜Ø#vwŒŽcĮ<[ĮŽą°ģģģqlQ LÄ@á}k_îåÂ9pãœ{×~ā=gīĩûĖzöY{­ÛÄnmé˛åEŽZ AH’Ô°Š)ĘÜS‘•ū°nØ 4@ĻXøĻgĪža÷îŨØšsž>{†žC† {š1Øã×YČÆ†0ßc6$`˙5iR§†ŗSU4lØųōå“mĪŨģwąsįNœđ}S§ XąČą­TPf׎]¸zõ*nܸĄ %ȟ??h@j@÷–,YRl($Lųoĸk׎aȐĄČ”-;&͜-Ų>ž Čņë,dųyQ<…ŗgbíĘ˜1}:5Š: ŊŌēpáČqjC ČéŪģwOjPĸD ”/_uë֍rÎÛļmÃĀAƒĐžs7tīĶWŋÄ. Ŋž{âũH6Pŧd)I÷ķEq' ĮáE+d$ēˇd¨ß1îF)Ŋ‡ÃCāuķž5D@0 Y3­÷=ģ…˙ŲŊ}+\4…ę5jüKkŪŋ‡ËaŨÚĩprŠü›ģģ;Ö¯_S§N‰ƒ&Mš„‹“%KąąÚøØOŸ>a˖-âí–ŗŗ3Zļl‰öíÛG˙ØąchßžŽœŊ씆xäĖœ‡59~…Ŧɖ…Š ?ģuFŅÂ1jäČHˇīØąkÖŦ}wuuN4eʔ:áJNW?>ÖĪĪO8éƒ AÜļm[áŦ#ļqãÆãÎũøgáŋQš Ë6āyõŠ(R@Å ĘU¨›)ō=q$ ĮáE+d“&ŋŽUí‘5/P)ę?vâhĒ2n_9Ȓ'ûWÄ×ΆˆŖ`!Ģ€•Ĩâ)ģļoÁŪ;t…j×̝ŗŒ’uoßZė°F # ?:tčPôčŅåʕC͚5eΈü*m&,Y˛SĻLAĢV­ÂõAáEЁû†-ė+eĶĩžäøu˛Ö÷|¨fÆuœ*bЀh͆Ō]†ĩͧOcĐ A"ŧ RĨJB”Ęɡ*ĘŊîØęįâŋ˜>}:*T¨nŒ•+WbÎÜyØs딸÷!^)×+ÂФĘrCÕjÕUp IDATÃĶR •ãđb˛ĶvØcŪ0 L5 œü˙3W<ãsö@ųš!˜Ô…ŦâLžÂW…æ¨=zđ€ĩuęÕĮŌųsqîÜŲp^ž|;vDņâŅø4DœŦÅ7oŪíđŌ›0ũVļl9Ėųw9˛åĖ)r|‡Õã×YČZÍcĄž‰ŌᨕĘaíęÕ"ūJŋŅ_ųô—=‰OÚ9ˆk;wîœÅ9sæÄęÕĢÃuwéŌ%tîŌĨËũ÷Ĩ˙ęÂjšZđn]\šá~9O’Ĩ‹f2åÜz™aFFōĮw`ŅH PYĀĨ)đŪ—…Ŧ0+­ËĀĀ@˘ô7ļoڀ§OŸ†3oŊ pĒI“&ĄpaÃĮņĶÎī!CÄÎlģví"‰Ų&ĸėoá7”Əí19~…Ŧų։Gūr…ķãčŅ#ȓ',×ë“'O@1¯[ˇna†n{öėA‹-ÄA†Ė™3ëēŋ}û6œĢšāúũG†’û39O˛Ĩ oÎ\ÛÅ+ČZ3tsvåŒzN€ŽØ Y3,„釤˜ØĒĨ‹ãíÛˇáīׯüũũą|ųrŖE1ŗ”‘6ô›ŊŊ=.üw—cfžę@Ž_g!ĢÎ5ļXĢ;ˇnŽî]:‹S°ÚFąWŨģw"3qâÄF›û—/_„Xϰ‚jÕĒéÆŲŧe V¯]‡Å+×mlî8öä8b?ú l2;x iä8<ŲBV{Ã{_0ŧH›(T(\°3_ŅHsyų¸u¸uxũ¨ö;āü;`/ō´YČJ{¸T|Õ°~}Đą]›pŲ h'6mÚ´&ąZ„cĮŽÅįΟÃíĖ=zk6lÂÄŗTLšM79~…Ŧ1V€û”MāØáC8{ü0æĪ›§ģ÷úõëâ ĨÔ2uŖĶĩ”á@?vŦgĪ^pĒåŠ*Õ8šˇŠ×#ēņä8ŧX YíÁÁ€į™0Á˜25Pĸ :)1v9ˆeķ ü<ø<>žÎ%ÖÄĀ’ČÎ[<ú.YČĘFŽĻ¨Øûĸ8xđ€ÎlЉõđđ0I8Á¯XQŦl5DÆmsqŠŽî} ˛“ŗšŗ­F& Į¯ŗ5ōbp÷ŌT)]{÷ėFŪŧyu7P‚o:øecĶc*m 9WŅ!‰dɒ‰WpÚFņ˛MÜÜpėü9]ņĩF& ĮáÅYČFėā™ā} xxK#,}Ÿ™sųJhvBSĻė͆~O$N$Hċ˙kSH¤_üoŋwąę÷–ŌghDôģW@æ\ņœĢ0ģ(&ƒtŌ,dĨŗRá•5+•ĮšÕĢtÅ(;ÁøņãE‘sˇ:uę`âĉēlWŽ\A—ŽŨ°īÄis›Æã+ˆ€ŋ“BČā$Cm3pžA=(J1eĶē5¸zî VŽtיDE č4mŅĸEÍf&UĩĄØ.*ļ m­[ˇAgüŪŦ…ŲėâÃãđ .d#vüSŗKúæ…æ‹(‰Qú?æ3Ē!Á@ü„@ÂD@öü€÷m€ūh úĄš I’é‰ā´šŸ)ŦÁ!+!Kܲqã§āģŠ8ËŠÃą~ũ:•QŊa2×ĸzĶÖ´i3Ô¨×õ˙n.ŗx\…ã×YČ*lņŦҜˆ.\ˆ˙ūû ,0;ŽnŨēĄL™2čÚUsX†(˜}I" Įá]ČJÅC‚—-}ũ ԈÚ‰4ģĩĻxÁBVęJŠî:—ßĘāČáCē^Tė€ĖFL/hΉQņ:ĐK™b´~ĩnũú8äaēŗæœ?39~…lĖ<ų #Xĩl |}ž`æŒēQRĨJoooPzs7J[S @ŧyķFgƟöEÖ<ųĐēŊ”25÷H_ŽÃSŒ•0/Ŗ^ÂBÖ¨xÍÕųņ#‡°ęßE8p`ŋ΄L™2rbĸ؁ĄæE•+WĨUÔļęÕk kŸ~¨â–1ÆPãq?ę# Į¯ŗUßúZ”Å5+—ĮšUaą\”kĐ××ĶĻMSĖ<û÷īlŲ˛NüRã˜.Å,0DŽÃ3ˆũüÚD*@vPĶ:jŌsU Íq|h=đÎhŠyf×XČ*nI aĐđũPļdqôėŲStG•ĩ¨€Ė¨QŖ ŅŊAû ,”F[,aîÜšđŧuφĪ7kĐAš3Րã×YČĒfY-ĪĐį>>h\ÛĪô*ÎT¯^ûöíC„ 3á¯_ŋŠ2¸‡ŌŲä˜1#öŸ8 ŽŠąĶZ ‘ãđ *d§í’&(“ÁÛĀŌņ€[O Œ‹æ ÖΟ€]*e. YeŽK­*]0.^¸Ú…ĨVĢV- 85k*¯Ü2ųųųķįcīŪŊÂVڝ­RĨ*Î˙w'ŽøvK Į¯ŗĩ„WéV.[‚'÷ībáBM,,å¤^GŽQ܌œœœ0nÜ8T­ZUØÖĩk7ä+R ­;tRœ­Öf‡g!Ģíôô^āÔ.`øb@GöÚ)āČfāg@ģš•ëĩZOîëgŽŲ˙÷Ā'? IMõ-*)ģqŽF “XĻ´^Á!@ģ!ĀŖ[ĀļEĀ÷ošƒaõÚ…ËË[v˛ōxŠāę˙<¯cčŸāÚĩĢÂÚoßžÂ´k}‚ „}ņãk˛x)Rŗ˙]†… _2Wąذ( Čņë,dų!2ļMc`ŋž t,Ô"žÂ7›aQ 1ä2Ė_´+ÖoV’™Vi‹‡gT!K ĻôæoЄ4ë ĖėtĄ ? ´YcÛ3vTĀ`z`ÄĀ!pá@bxāl`÷ āĩĐq¸FÔÎdĖ 4íŒët ä.ŧzĒéĐ }Xiå˛1"RÛ˙Ο û¤‰Đˇo_aúĻM›DIī7*v*Mš4o~˙]“­`úŒøöčÜŖ—bmfÃLC@Ž_g!kš5áQ" BĢÆõāq’˛ģiZîÜšÅéÚ\šr)Ž×Ŋ{÷Ä)[*“Ģm•*WÆĻ=͒įVq€Ėh‡gT!Kydgôfîŋ#ûíkhĩ-đŧ| ˜˛ x÷RŽ0!´ô1íĐŽœŒ^Lî 4č,Ŗ1ųÄvāé \ `ûŋ°…aSY2Vŗ‹[ĨôU`!+•JŽÔģ'\œĒ S'Í[ĸaÆĄdɒhÖŦ™bg@EgnŪŧ‰˙ũīÂÆ%K–āÔŲķ˜:;Ŧ0ŽbgÌJ@Ž_g!kÔĨāÎEāéãĮhҍ.ŧ=—ŧūĩŨˇxá tĄŠō5{°ĻB“žĀ¸@ˇ1@Ž"€¯0í`Ā, céËũk!ÛåÂK €í:e!+’ą/Š]ĨÜW,×UĖ*Qĸ¨,íŧyķ°lŲ2qˆVÆEļ<~üXd  \Ų‹-Š•yrEoŠ–.]Š;w‚Ēūõũ§%­QŅĘŅM™`¨Ņī:wÁū“g¤uĀWY,~…ŦÅ> ŸØ=ģącĶzėÜĄų˙ÉíÛˇ‹„ŨŠS§VŦĶĨŲŽ;Šjõ4D“VmQËĩŽÂi[ļy2^ô“N–Н2Ô ZųB–ĒoQŖW˙ęâĻÉX@Mģ#K1˛´Ëúâ1ĖH•xrh3HSáW;˛T(aĶ<Āë†Ļ—]jÍŊÍ˙ŧūĶ„üøĻIũUĢPĘIŪ"G˛-ː@rĪd•Ø) Y‰ ŒyY‰|9qũÚ5888ˆaqíÚ5Œ9‡FĨJ•°vmh JÖRŅōką˛īßŋeŧiL‡BÆ&Ož,yš/_žDŠRĨđâÅ qũ^˛T)\ŊûPr|Ąeá×YČZæ# üY­ZžŪ÷ncŅBMŦ9ROOO*ÖéöčŅCėvtīŪ]ØÜŊGØĨI‡ē )¸[øúõëv͛ø¨(aš—$ ŊŽB ´‚v’&?!)´@ Øĩ  Ôd!ˆMģrBSĻļHyMz¯Ĩã€üĨäÅÁF7n˜mNéF؅îÂŌ]ÄeE憞ĩŗÕûŽ˙3ŊŅŖßĶ'M–léb÷ՉCBBú,žkŋ‡‹ŸĄ˙oú×č˙ šļŪũû ũÆ‰0Ž#üũÚžBĸ¸V÷Y${5ļĮho”ãk8„›1ŅģV˙ŗˆcˆĪ@•Œ#Ī-J{"Rn=}ZˇęÎÎÎ8~ü8ēt邔)SŠœ˛Tˆ Y2Úl ,JwHB”ü/‰IĘ?Ka^´nmÚ´qļÔ¨8 }FŠX ÅáŌ4íŽ,áææ&îŖÃf Ąb”˛0^ŧxĸ:âŦYŗ`gg‡Úĩk‹l ĮŽũīÚĩKØŠm•ĢVÅĻŨbķ_ ßcAXČZĐbZęT–.œ‰âëŠ Pė)9A üWĒĶ­PĄ‚HŋĨ-Œ@6Μ5 I’Đ&7s ?~ŧzũ ;‰÷kcŌ‘đŠđņ7Ŗ‘4ųpƒŲu35‡ģēŒrˆĘh°îā{€Ļ„mŪâ€[/Íî¯!Z˜%1JÂ^?qķGü$-EšRīģūĪÚĪŏŋbÕjÕãŲØØĀÖÖFėÛÚڊƒēī6ļ°‰øoúŋkļą wŨНĐkmlĸūŒļhôī?Ķ˙ĸŸ>‹d/)tēV’ŊáįvOdd¯–‰Öž(m e ÉŪĐ>Ôt†įõëQîČfĪžׯ_ģ¯$POŸ>ūųEŠÁĢW¯„­QŖHü>ūūū ”ƒäë¨`AōäÉÅõ@ų^ķåˇwīŪ‰øVmhūŽ,Ū:ūŧȚ@Šĩh€vk—/_.„, cÚĻb äWyGÖ˙Q[V,d-k=-r6ŅíČ*Õé’3.]ēt¸Ų\ ŖmĮΚFj™” ‡GÕ+čĨvGö3€(äāäÃ^j#mG–*6Œ0ęî[†_įĐu/ĩz­.FVŠN—cd•ųŧÉpx H˜ôŦ&{< =FV™äYõa/ŗTË4 Z ļü Ąc˛ û’_e- ]QōŠC‡E–,YÄN)UúĸƒY˙ũˇ˛ôv‰„ė‡tB–Äí‰'D&Ž^^^ēŨ^íīkÖŦ‰RČR<.§˛7nÜ;ŧ$”IČjC(KÁâŋAyšŠqÖc?%ęé_†_g!Ģžeĩ,K•ĩ€âļ”ęt9k2ŸA‰/mčëōÛĄ;°ZĢ Ų°åí `jhv‡ÁV…ŦHÆžd`īž¨Ž—G–˛PL?ÅŠ’OĨW˙Ŋ{÷ģŖtĀjæĖ™;vŦ.´ĀÅÅEėšŌ}Ÿ>}a;wąąR„,õåįį'ba'L˜€‹/ŠĐĒŪE}HĻŨ]}!KydoŨē%55Î#kė§D=ũKôëbBœ~K=ëjQ–>{ōÍÔÁcoo1/:`@¯ĸč€R.į‘Uæ#(Ãáåp˙ŗ`! Åŋ–°ę,d%@2ö%˙.˜‡”‰čbø7oŪ úĸVZŸzá”/_^ÄË+V,œĨxÕ?ūøCæĸ`͛7ŸS,¯!Ka”Ņ…ĒtQƝ!C†ˆrãK!YĒ@Yiô…,]Ûļm[4nŦɁL‡Į‚.=¤Fĩ›*÷o.2ü: Ys-’ĩKέECWœņđСț7/Š/ŽĸE‹ŠŨ%9ŨB… Ę)Ōë8mĢXŠ26īåĘ^æ~–å8ŧhl5ސŊv X6!ėå’Í’hØES Ú¨ÖĀĮwšTZÔRĻjļ*1­[Ėä,+ Y9´ŒtíÍžÜģ'Ž_ŋ&F 1JņŽßŋ7Ԉqī–2ĐaMqā@áÂE0wé ä/X(îsĒ& Į¯ķŽŦĒ—ZŨÆˇkö;ú˙ŲT†ĨlĄŽūũû+nbĶĻMÛ7o0u*Ŋq…Hŗđß%Xžn“âlĩ6ƒä8<ŗŲũká‹5CS~XĒūĩm‘Ļ:WúĖ!Ûz ŋ¤æš‡7Y€q́ÔŒŗœ,dÃÕĖŊ–-”gĪž~”í~’?­UĢ–™-‹<|ÄĒ^´ė\ÍįƒĻŠ7ķãđĸąŌxĄú;˛ZŽonœúNĶŲ΁xņ€  đ;P¯PGķđFi,d‚Õܝž8zî‹āāÁ°‚´;K%Ā3gÎlnķtãSy\:LFģ°ÚæâRŨû@e'gŨɆ˜€ŋÎBÖ|ëÄ#¨Uå7ŦrwąąÔč$íķįĪ1c•{WFŖJ5šråŸū) ē|ų2ēuīŊĮÃâ{•aŠuZ!Įá)GČnî^zūyGÖĮ X8hŌ(YÕ8‹ĘBÖ8\ĐkĩōĨpčāA]xÁēuë@¯ņõË͚ÛL:HFg(Ÿ,5ļõ4ĀáĶĖm¯rü: Y…,šĩšAá/ŧböl*ķŽiiŌ¤ÁƒÄ Wsˇ×¯_‹Dâ”Č[ÛūčŨTĄeÛöæ6Į Įá)FČ.˙Hë4čYȒ‘kg Í(– Y#@UF—;ˇmÁņũ{ąqãA”Ą€ŠĐAZsˇˆEȞ&MÜP§QÔmČåžÍŊ>J_Ž_g!Ģ”Uŗb;ĘĖ‹ķįÃ^}Q‚ėk׎‰˛‰æn”¸ûˇß~ųŠŅÎAU'gœŋ–ŊĀÜ6ZûøržŲ…lP pá°í_`ä UúČBöÍs`ö` ^{ ŧ‘鰐ĩč˙l"Vų"JųaŠĒ—š…Ņ›7Ú  véŌ%tīŲ {2ˇi<ž‚Čņë,d´pÖjʖ ëpŅã$V¯^ĨCPˇn]‘$›z›ĢQÁøņãE†mkŲ˛ĒÔ¨…FnšWbÜĖO@ŽÃ3‹]öˇfw•ZÂD@–<@ũŽ@ļ|šŖŲO ŽÄIßjkŽ1Vc!k,˛Šč÷ÔņŖX:o>¤ŗgÕĒUâ -3WkŨē5ȡˇjEö4ÍŲšū8Ģ)ŒÆ\“åqãD@Ž_g!'Ô|ŗĄ8•-‰;ļ#ūüē.Š"L@@âĮo¨a$÷ķíÛ7QœÆ×6Ē5ŪŧE 9{Ir?|Ąņ ČqxŅXcœÃ^Ɵ~ėF`!;n*ē믁}ŅŽe TĢVMgõ AƒDq*t`ęFšÁ)øäɓuCSÕą [ļáĶ˙1ĩ9<žÂ Čņë,dž˜ÖbŪ‰ŖGpęđ,\0_7e*ĨHÁSnS7*•Kå ( ē[÷î¨^ˇ!ŸĒ5õbÄ0ž‡ĮB6” Y…=ÅÆ1'b^Y…^ëûûû›TĖ’ˆÍ!úôéŖ›(į5Κ[J¯rü: YKYu ˜ĮœéSaˆ˙…ÖŨĻ)QLjĶĻME•ŠZ™2e°eËd˖M7äĐĄÃ0Y ôę7ĀTfđ8 Čqx,dYČJ|Ŧ,â2˙QŽH~øųų…›íĖŌAV 70vŖÍJĻŋKcŌΰįƒĮHš,™ąMāūUH@Ž_g!ĢÂļd“ģˇoömZ‹Ô,ÚFq]íÛˇĮŨģw…ķ3VûøņŖmذa¨‚ļŅīˇlÃ‚åæ‹-3֜-Ą_9…, YKxæåĖáÂŲ3˜8f.^ŸÚОŦ_ŋ'NÔĨ?”ĶoL×Rv‚ĄC‡ĸcĮŽábbéžRĨJcė”é(]ļ\LŨđįVJ@Ž_g!kĨ‰’§ũ[ąBØˇwčõžļQnY™” ąAƒ7įΝ:ąėāVä€Nû6mÖW< >&whr Y˛†yęÔÕËcī‡čÛĩ3.^ /fÉŋuęÔIøÖ)SĻ kÖŦqžŊE#ëååwww]vmĮ$bē¯F–lŲã<w`šäøu˛–û¨vfÁÁÁĸPÂōĨKQļlŲpķhÛļ­pĶ§OGŊã–*†Ņũ 6Œō­Ō2d(rä͋˙Mã[ø(mJrü: YŖ-wl(‹įÍÆŠaÆôépss‹˛Ûũû÷ƒBŽ_ŋ.v čKëtĩÂļxņâbˇV­¨Ģ%mÚ´  B×^}Đĩ—‘Jƒ ÷Ŗ# ĮṐe!Ë˙éhxœ8ŽIcG!wŽ\˜1cēAâcĩl)N–|Š÷ã'>v;ā‡N69~…Ŧlŧ|ƒ9ø<}Š Ŗ†ãÕsát+WŽl03h‡vāĀAȜ-FN˜„L™3ŦoîČøä8<˛,d˙DĒk„ŨÛˇaŪĖŠHŸ.=6l B ôSJ ÅĀîŪŊ;wî‚ī›×č7ø/ÔmØHęí|G@Ž_g!ˏĒ\ģrŽdI“ĀŲÉIŧŌ*T¨ė9P•.Šë:åá/_0rÂD+QRv?|ƒų Čqx,dYȚ˙‰UϧŽÃáũûpx˙^ØŲŲĄC‡öâģ6Œ@'Ģ#K?û}üˆ•î+ņųËÔ¨íŠu\šhŒ2—XUVÉņë,dUĩ´lŦ–ĀÕKqøĀ>ܡ?žGĮ.]:]|Ŧ6FVf ũūæÍŦpwGâ$IPËĩjÔŠ‹ĨJ3Xãđĸ™fVdĘqׄÕHV1“M˙đ6s߂īŗ"1^ķYėSĨēsûņsÎl3+U\qįÖMœõ8…{wnãī+ŧzųžž¯;Ox=x€ āāčˆtˇ@TĒâ„üåo(¨irü: Yŗ,jHŊáäą#¸sķ&^ŋz)œîĢW/…Ķ}čåGátĶ;8"ÁÂprŠŽl9rŌîˌä8ŧhÍ´ˆģâ#a"3ÎÆDCßžŦšrŸü ‘‘ž…Ŧ‰–‡aÖB@Ž_g!k-OĪ“ X(9/ZöéŧŅeTvä+ÄaĄČ€Ã3{ŨņæEGĖ‘…Ŧ rL€ „ã×YČō“Ø€Ē Čqx1ŲͨĶÚ •ęК‡$ã—˙ˆ+'HÄŽ•t}ôą5Dî‚ 0˛ü 0&`… &dĻpjŧM˙Hiņ§õy†ĮwĒđ6Ā\YČ"wÁ˜ Y~˜°B˛€]šč::7rZđÁ•Cëã;Â˙]/=.,d ’ģaėVø,IDATL@C@Ž_įĐ~j˜P59OÂD‹!C–ķŊ"ą„kÕwIĀ`XĶúaČm,dÕ÷$°ÅL@Ņäøu˛Š^J6Ž 0˜Čqx1õ%>ĪUh˛å&Ŋ )ö$ mô‹æ ņĮįđüáqŽÅBր0š+&ĀxG–Ÿ&ĀŦˆ€Á…,ąŗOˇ]ëÂĩ­åėĖÎėˇ/ģāũĢÍ~ÖLûŠ€¯x˙Ē˙XÎ]Ęm,dĨPâk˜L@Ž_g!++_Č˜€ Čqxq˛?EęŽH4rļGÕɑĢpœē3ĘÍ×<€SģđŌû5>ų5pÁ(ã„ī”…Ŧ ķLšČņë,d­éÉāš2 $ Įáhú‘2Í_°ąq„Sãä(W°Km ŽcŅÍ̧€Įîī8ŗĪÉRœƒßÛÉöĮĸ§ØŪÂB6ļäø>&Āĸ$ Į¯ŗå‡ˆ 0Uãđ <ŅŧH”¤RØ×FāĖČ_*ō•Hˆ,dÍĮžGfL°ĩ€Eä)0& Z,dUģtl8`J ĀBV ĢĀ60&`­XČZëĘķŧ™0˛Áȝ0&Ā$H ā[čÕ,d%c㠙`‘ °å§‚ 0&`Zŗäđ€üú¨  ;€ŪĄ??7­I<`L@XČĒsŨØj&ĀÔK €íB\@ ÜPFŊSc˙`Ļ%ĀBÖ´ŧy4&Ā˜ø@áPŸ$đ @u`L€ H#ĀBV'žŠ 0&`H´+ģ@*ŊN/(oČA¸/&Ā˜€Ĩ`!ké+ĖķcL@Šôwey7VŠĢÄv1& h,dŊ™¯¯oĶķhiŌ¤yôæéͧûĖķ]|Pū•Bs++à ļBŅB`ã‘É.ņ=˛Š^&6Ž 0%pppčicc3yäȑvUĢVEĒTŠŦĶ"Ųđöí[>|8`Μ9|||(dáž" eŖ˜@/üž,…MgÃ$°ąéšŅ.ÉR˛’hņEL€ X+ėŲŗˇ(\¸đ‚Ũģwë§ĘRŽŌĨKŋžråJ÷Ta0iÕXČZõō˟< YųĖø&ĀŦŽ@WW×m{÷î֙͠gɒåĨOV3Pë ZŨ,d­g­ 2S˛Áȝ0&`ÁwŦ[ˇŽĄ“““jg9eʔ€aƍđj'Á†[}!ëh—$Ú9˙ Iß6ώĘVÁMm“üü=(äĶ÷¯ Yĩ-=Û˘€‰ Ä˕+×C//¯l&× Ã]šrnnn‡?~\Ë sgLĀĀXȨBģc!ĢЅaŗ˜°8™‹-zÄĶĶ3ŸšgöäɓęÕĢ_õōō*­æy°í–O€…Ŧå¯1͐…ŦuŦ3Ī’ 0ķČfooũÇöæ7%ö-^ŧøG???ÕV‹ũŦųN5ˆ(dˇnŨŠšsįâŨģw A–,Y0dČ8;;#ĻЂE‹‰{ŠŲÛÛcūüų(^ŧ¸ø}âĉXąb~ūü‰6mÚ`ܸq°ą {ĶŊnŨ:Ŧ^Ŋû÷S;MëŪŊ;VŽ\‰øņã‹ßK–,‰S§NEœ={v¸ģģƒÂ‘čÚW¯^!mÚ´YŽĨK—búôéđņņAŠ)аaCL›6MüüđáCŒ3kÖŦ1ČXÆė„…Ŧ1érßL€ 00,dųi`&$ /dįNž€ÛˇoŖbŊØļm‚‚‚(DģwīF‡ĐŽC§_ÆČR8 ‰ŧĢW¯"}úôXŋ~=Ǝ‹{÷îaßž}1bNŸ>-„f:uĐĨK´jÕ ūūū9r¤ĸ*Ttŗ/R¤ˆŋĨKG˙bÃXBö… ¨_ŋ>Ž9‚ĸE‹ qOvÛŲŲ M˙NļŸ?Ū„+ģĄXČÆŽßؐK€…Ŧ\b|=ˆ}!Û¸Ļ3ļoß.vQ˙ûī?!Ü6l؀Aƒ 1yáŌå{ģQzôččĢzõęÂooo+V ?~DˇnŨ7o^ |;Ī4ĪĖ™3‡ëxīŪŊčׯŸį´KíŲŗgâwę‡8ŅīuëÖ÷ĶõMųŽSĻL‰Yŗf ´ =zôh1_Ģ`Á‚b.ŠS§FŊzõĜˆņûũ÷ßÅõğú&!_ĻLŧyķŊ{÷ƃÄįôGíj“ØîÕĢâŋĘgMķ"Ū?;ŪôG 풛ZČRåš9ĮáŲä[™`j$ĀBVĢÆ6Ģ–€žŨšn% .ŒūũûãŌĨKáæ4tčP´lŨ&¤xŅ"1ž|'!×ŦY38:: UģvmąŖÛĸE Ņį‰'đįŸâƍē1ļlŲz¯Ũ‘%ņ:pā@!>säČ!^áS؉H)BÖ××WŒAâ1QĸDbĮwņâÅ8wô3Ÿ$ItęÔIØúŋ˙ũ/\×$IČŌîrŲ˛eŎ1ÍEģCŦŋ#{íÚ5´oßBĒĀlŪŧš¨Ä€vŦ—/_.įÔŠSqķæMŦZĩJŲäɓ L׿˙ģví;Á$„I˜’č'ëęę*v„ÅN9‰YÅdÍ%Ož<âZāÄķĮčÚĩ̘—}:“f- Ø0o[ĨZ׆3&K,dc Žocą!QȒ`ĸŨOZ/_žbŠذaÃĐŧe̐ŊF+d?}ú„ļmÛ"a„b‡5A‚¨YŗĻ‹úB–Ä2‰?m‹(d#΅Ä[˛dÉD\*Åíęˇ_íČ>}úTŧū§{Μ9#v˜i§“„,͏Ä-5úŽY¸pa”?ū,î§/ĩ... x`}!K1Ā˙üķ2eʤëãõë×BĐvîÜY뤒ĀĨF!$ é˛$úÛĩk'ūxŅ.1Åk™§šįΝ[aj_ŋ~ģÂ-[ļ÷Ķ\ŠŅ<¨būüų…´žųōå3ųŽ,Ųō €f›`LĀz°ĩžĩæ™*€€žũŊV5ĮēcĮ!ĘhGoŌ¤Ib72ĻĐš ŊoĐ Oú‡šhą@b‡• \ځ¤ØÛ_ Ų˗/‹­ø%!›4iRqč*C†đĩRĸ˛›Û´iSąģL¯÷I`ĶŽ. XŽÔ7‰Q­Õ˙]kĶėŲŗEŧ/‰EmŖWü4>}'!Ž‘0aîßŋ/Ŧi‰KŨnAņĀ$hĩ"”Ä1õMB–ŋŅ<ĩB–lĨ]Z­ĨVš;íRįʕKôņūũ{!zoŨē%vii^ÚF!ĮŽ_d…'TqŠiŌY˛åNhhÁ]<įl`LĀTXȚŠ4Ãč ŲĶ&ŠKąô:Ÿš§§§ˆÁėØąc´‡ŊčU> ^:ŅO;„ú+ÅĢRÆzÍO;‰­[ˇÖ ;ē6âŽ,ŊR§ØS % 1H;ž‡ÆņãĮ#­[TB–bPīŪŊ+3ÅĨöėŲ'OžÄ;w$ YzõO‡Ô(ގ|ųōâ•>ũLB˜D;‰~ =¸~ũē8đE;Ī´ËJ; 7n1ÁtŨĖ™3ÅīRAąļēv]iwWАĨp + $žPĨJ6@"]_ČR( l¯Ô(līҧŸÉ…ėbt Nc 7&Ā˜€u`!këĖŗTˆéˇč°íD’0%H‚– čuutéˇūúë/L™2Eėę7ę‡^‹ĶÎ.‰@ÚåĨØÎ3fĀÖÖVwiTĄ3K"îĄøĪe˖!cƌ’„ė—/_„ Ļ9PĻŠ#]˛d‰HÍEiÁ¤ėČŌ@ ,_$Héĩ~åʕ…í´ÃLĄ Ô/5ÚĻPúŒYҜiJFxøđáØŗg˜3 ]š›ö°WL;˛$d_ŧx>}úqNąģ5L)tA_ČRúŖƒ;…/Đ.qŗI˜ÄäBÖĀ_4Į˙¸1&ĀŦƒ YëXgžĨBpA…,„‘Í0uÖítļ ZŨ^`äæî™P ˛ŠY 6ݐĩ†U6}e/-UGt¤ĪÁ:0ķ,™``!Ë0!}!›<‘ς֝ZH‚ml`B;y¨¸ °?ƒcLã(66]3Ú%Y*§#JÁEŋǏã|`L@Ũd Y:Kq`”&ČÁ!ėo~JDŠq(&-6NLĶ)bŠŖ‹MãĩąĄÆ÷˜ƒ€ž5Įø<ĻĘÄBČŌ É;ĪÍ+ģĀ™PqŦ˛éŗšL€ 0˜°‰Î H€…ŦaZCWą˛Z4Ψ,FEؐ= D ėB$°5Õ5χĻlĒąL5Ī)ú9ĻuˆésSō5ÔXj›“{ĻH‘ĸ ŋŋŋŋ%JLÆ´#KšŠÄæĄC‡ÄŠc*÷H;ļT/ũėŲŗ"ˇ%pϜŒãĮ)ôwdŠíÎRYM:ą,ĨņŽŦJ|ŧôh,íÁV‚Álƒy ØÚēg´KrZNhAtSÁrö1õĶį4†ŠŽ1Õ8<§čõ˜Ö!ĻĪMÉ×Pcņœ~ũL˜Š”qRĻLšØĪĪ*ÆØ´B–rSę7JOSĢV-Z@å)ĮãÖ­[Ež=zˆÄã”+“ŌālŪŧYTJkSŠR%Q’ęŧ“xĨt:TZ“Ž)Q"Ļ=ƒ0 XČÆ¸t|`*& řĢxzl:`L Ö Z@‰Ã)19Õ.§F ߊŌ%#§§čÚÖ¤IņíÚVĢVMä ¤j<´ƒ+§ą•C‹¯eL@mXČĒmÅØ^&ĀLEĀāB–vY‡Ē˛7n܀““6mÚ$ū]_ČRiMúŒ’œ“ Ĩį”xœJvjŸKÁBV %ž† 0ĩ`!Ģ֕cģ™06ƒ YǁNå.)´€â`ŠT%•‡\ŧx1ōæÍ‹mÛĒ“N%6Šė$…,hŗP]xĒŅNe(Šūš”ÆBV %ž† 0ĩ`!Ģ֕cģ™06ƒ ŲīßŋcȐ!"ÅÆ–.]sįÎå!=<>>S\Ršl`LXČ’&÷Ř€5H =%úĪ žBŦa!xŽL€ 0%:b^&Ā˜`L€ 0&#˙7fą—ŧĖIENDŽB`‚django-q2-1.7.4/docs/_static/favicon.ico000066400000000000000000000353561471170400300200150ustar00rootroot00000000000000 h6  ¨ž00 ¨%F(  :ĖĄ:ĖĄB:ĖĄ :ĖĄŨ:ĖĄö:ĖĄö:ĖĄŨ:ĖĄŸ:ĖĄA:ĖĄ:ĖĄ:ĖĄ‹:ĖĄė:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄë:ĖĄŠ:ĖĄ:ĖĄ:ĖĄĢ:ĖĄū:ĖĄú:ĖĄĀ:ĖĄt:ĖĄN:ĖĄN:ĖĄu:ĖĄÁ:ĖĄú:ĖĄū:ĖĄŠ:ĖĄ:ĖĄ:ĖĄ:ĖĄ˙:ĖĄđ:ĖĄu:Íĸ:Íĸ:ĖĄv:ĖĄđ:ĖĄ˙:ĖĄ‹:ĖĄ:ĖĄE:ĖĄë:ĖĄû:ĖĄs'ūžc_b bbd[bbdšbbdšbbdZc^b *öš:ĖĄu:ĖĄü:ĖĄë:ĖĄC:ĖĄŖ:ĖĄ˙:ĖĄž8ŌĨeY_ bbdšbbdúbbd˙bbd˙bbdúbbd™fX^ 8Ō¤:ĖĄĀ:ĖĄ˙:ĖĄ :ĖĄā:ĖĄ˙:ĖĄrbbd]bbdúbbd˙bbd˙bbd˙bbd˙bbdųbbd\:ĖĄt:ĖĄ˙:ĖĄŪ:ĖĄų:ĖĄú:ĖĄJbbdbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd›:ĖĄM:ĖĄû:ĖĄ÷:ĖĄų:ĖĄú:ĖĄJbbdžbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdœ:ĖĄD:ĖĄø:ĖĄø:ĖĄá:ĖĄ˙:ĖĄpbbd`bbdûbbd˙bbd˙bbd˙bbd˙bbdúbbd_:ĖĄD:ĖĄų:ĖĄā:ĖĄĨ:ĖĄ˙:ĖĄģ7ÔĻ e[`bbd bbdübbd˙bbd˙bbdûbbdžbbd:ĖĄD:ĖĄų:ĖĄŖ:ĖĄH:ĖĄí:ĖĄú:ĖĄo˙ôc`cbbdbbbdĄbbdĄbbdabad:ĖĄE:ĖĄä:ĖĄF:ĖĄ:ĖĄ‘:ĖĄ˙:ĖĄí:ĖĄn:Íĸ :Íĸ:ĖĄ1:ĖĄD:ĖĄ†:ĖĄ:ĖĄ:ĖĄ°:ĖĄ˙:ĖĄø:ĖĄģ:ĖĄm:ĖĄG:ĖĄG:ĖĄm:ĖĄĀ:ĖĄĄ:ĖĄ:ĖĄ:ĖĄ:ĖĄ:ĖĄ’:ĖĄī:ĖĄ˙:ĖĄū:ĖĄų:ĖĄų:ĖĄū:ĖĄ˙:ĖĄŖ:ĖĄ:ĖĄ:ĖĄH:ĖĄ¨:ĖĄä:ĖĄü:ĖĄû:ĖĄã:ĖĄĻ:ĖĄ<øāÃÏņžy80 0 8ž}ũÃĪāø( @ :ĖĄ:ĖĄ:ĖĄ]:ĖĄœ:ĖĄË:ĖĄæ:ĖĄņ:ĖĄņ:ĖĄå:ĖĄĘ:ĖĄ›:ĖĄ[:ĖĄ:ĖĄ:ĖĄ:ĖĄ,:ĖĄ‰:ĖĄÖ:ĖĄø:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄø:ĖĄÕ:ĖĄˆ:ĖĄ+:ĖĄ:ĖĄ:ĖĄ{:ĖĄã:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄâ:ĖĄy:ĖĄ:ĖĄ,:ĖĄˇ:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄĩ:ĖĄ*:ĖĄ7:ĖĄĐ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄâ:ĖĄˇ:ĖĄ“:ĖĄ:ĖĄ€:ĖĄ“:ĖĄ¸:ĖĄã:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÎ:ĖĄ5:ĖĄ-:ĖĄĐ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄÉ:ĖĄi:ĖĄ$:ĖĄ:ĖĄ:ĖĄ%:ĖĄk:ĖĄĘ:ĖĄü:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÎ:ĖĄ+:ĖĄ:ĖĄ¸:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄí:ĖĄ}:ĖĄ:ĖĄ:ĖĄ€:ĖĄî:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĩ:ĖĄ:ĖĄ:ĖĄ:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄã:ĖĄS:ĖĄ:ĖĄ:ĖĄV:ĖĄå:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄũ:ĖĄ{:ĖĄ0:ĖĄä:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄė:ĖĄSbbdbbdbbd:ĖĄ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄé:ĖĄų:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄvbbd^bbdūbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdūbbdZ:ĖĄ{:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄõ:ĖĄų:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄvbbd_bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdūbbd[:ĖĄu:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄö:ĖĄî:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ‰bbdDbbdöbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdõbbdA:ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄë:ĖĄÕ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŽ:ĖĄbbdbbdÕbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdŌbbd:ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŅ:ĖĄ¨:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÛ:ĖĄbbdbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd|:ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŖ:ĖĄh:ĖĄú:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄ_bbdbbdŋbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdŧbbd:ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄų:ĖĄc:ĖĄ(:ĖĄŨ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŋ:ĖĄbbd+bbdĀbbdūbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdūbbdŊbbd):ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄŲ:ĖĄ$:ĖĄ:ĖĄ”:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄrbbdbbdbbdØbbdøbbd˙bbd˙bbdøbbd×bbdbbd:ĖĄt:ĖĄ˙:ĖĄ˙:ĖĄ:ĖĄ:ĖĄ6:ĖĄč:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄį:ĖĄIbbdbbdbbdHbbddbbddbbdGbbdbbd:ĖĄt:ĖĄ˙:ĖĄå:ĖĄ1:ĖĄ:ĖĄ‡:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŨ:ĖĄI:ĖĄ:ĖĄ:ĖĄ:ĖĄu:ĖĄ˙:ĖĄ:ĖĄ:ĖĄ:ĖĄĀ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄį:ĖĄp:ĖĄ:ĖĄ:ĖĄs:ĖĄM:ĖĄv:ĖĄŊ:ĖĄ:ĖĄ5:ĖĄØ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄú:ĖĄŊ:ĖĄ[:ĖĄ:ĖĄ:ĖĄ:ĖĄ:ĖĄ]:ĖĄŋ:ĖĄû:ĖĄb:ĖĄK:ĖĄ1:ĖĄ@:ĖĄØ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄú:ĖĄŲ:ĖĄĒ:ĖĄ„:ĖĄp:ĖĄp:ĖĄ„:ĖĄĒ:ĖĄÚ:ĖĄú:ĖĄ˙:ĖĄ˙:ĖĄa:ĖĄ:ĖĄ:ĖĄ5:ĖĄÂ:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄa:ĖĄ:ĖĄ‰:ĖĄę:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ`:ĖĄ:ĖĄ7:ĖĄ—:ĖĄß:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄŨ:ĖĄ’:ĖĄ!:ĖĄ:ĖĄ):ĖĄl:ĖĄ­:ĖĄŲ:ĖĄō:ĖĄũ:ĖĄü:ĖĄņ:ĖĄØ:ĖĄĒ:ĖĄi:ĖĄ':ĖĄ˙đ˙˙€˙˙˙ü?øđøāüā˙˙Á˙˙ƒƒøÁƒāÁ‡ĀáĀā€ā€ā€đ€đ€đ€đĀđ‡ĀņƒāņƒđņÁ˙˙ķĀ˙˙ķā˙÷đų˙ø˙ü˙ū˙˙€˙˙đ˙(0` $:ĖĄ:ĖĄ%:ĖĄW:ĖĄŠ:ĖĄĩ:ĖĄŌ:ĖĄã:ĖĄë:ĖĄë:ĖĄã:ĖĄŌ:ĖĄ´:ĖĄ‰:ĖĄU:ĖĄ$:ĖĄ:ĖĄ:ĖĄ:ĖĄ^:ĖĄŠ:ĖĄŪ:ĖĄ÷:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ÷:ĖĄŨ:ĖĄ§:ĖĄ\:ĖĄ:ĖĄ :ĖĄ{:ĖĄÔ:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄĶ:ĖĄx:ĖĄ:ĖĄ :ĖĄg:ĖĄÕ:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄū:ĖĄĶ:ĖĄd:ĖĄ :ĖĄ,:ĖĄ­:ĖĄų:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄų:ĖĄĒ:ĖĄ):ĖĄ:ĖĄL:ĖĄ×:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÔ:ĖĄH:ĖĄ:ĖĄ:ĖĄ_:ĖĄé:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄé:ĖĄŌ:ĖĄž:ĖĄŗ:ĖĄŗ:ĖĄž:ĖĄĶ:ĖĄę:ĖĄü:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄį:ĖĄ[:ĖĄ:ĖĄ:ĖĄ_:ĖĄí:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄÛ:ĖĄ™:ĖĄX:ĖĄ+:ĖĄ:ĖĄ :ĖĄ:ĖĄ:ĖĄ :ĖĄ:ĖĄ,:ĖĄZ:ĖĄ›:ĖĄŨ:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄë:ĖĄ[:ĖĄN:ĖĄé:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄū:ĖĄĶ:ĖĄp:ĖĄ:ĖĄ:ĖĄ:ĖĄ!:ĖĄs:ĖĄÕ:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄį:ĖĄI:ĖĄ/:ĖĄØ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄđ:ĖĄ‹:ĖĄ:ĖĄ :ĖĄ:ĖĄņ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÔ:ĖĄ+:ĖĄ:ĖĄ°:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÜ:ĖĄT:ĖĄ:ĖĄ:ĖĄW:ĖĄŪ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĢ:ĖĄ :ĖĄm:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĶ:ĖĄ;:ĖĄ>:ĖĄÖ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄú:ĖĄh:ĖĄ$:ĖĄØ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÚ:ĖĄ:bbdbbdbbdbbdbbdbbd:ĖĄ>:ĖĄŪ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÕ:ĖĄ!:ĖĄ‚:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄī:ĖĄQbbdbbd)bbdqbbd¯bbdŅbbdŪbbdŪbbdĐbbd­bbdobbd'bbd:ĖĄV:ĖĄņ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ}:ĖĄ :ĖĄŲ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ†:ĖĄbbdbbd‡bbdãbbdūbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdūbbdábbd„bbd:ĖĄ:ĖĄ‹:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÕ:ĖĄ:ĖĄf:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÎ:ĖĄbbd8bbdÆbbdūbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdūbbdÃbbd5:ĖĄ:ĖĄŅ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄa:ĖĄ:ĖĄą:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄũ:ĖĄjbbd9bbd×bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdÔbbd5:ĖĄo:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŦ:ĖĄ:ĖĄ-:ĖĄã:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÕ:ĖĄbbdbbdČbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdÄbbd:ĖĄ:ĖĄŲ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄā:ĖĄ):ĖĄb:ĖĄú:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄbbdbbdbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd‡bbd:ĖĄ:ĖĄ–:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄø:ĖĄ\:ĖĄ—:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄų:ĖĄObbd/bbdåbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdâbbd+:ĖĄT:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ:ĖĄÂ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄã:ĖĄ$bbdzbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdt:ĖĄ(:ĖĄæ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄģ:ĖĄŪ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÉ:ĖĄbbdbbdˇbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˛bbd:ĖĄ:ĖĄÎ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄØ:ĖĄī:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˛:ĖĄbbdbbdŲbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdÔbbd:ĖĄ:ĖĄ¸:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄę:ĖĄ÷:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĻ:ĖĄbbd%bbdæbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdâbbd":ĖĄ:ĖĄŦ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄō:ĖĄ÷:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĻ:ĖĄbbd&bbdįbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdãbbd":ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄķ:ĖĄđ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄą:ĖĄbbdbbdÛbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd×bbd:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄë:ĖĄá:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĮ:ĖĄ bbd bbdŧbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdˇbbd:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÛ:ĖĄÅ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄā:ĖĄ!bbdbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd|:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄŋ:ĖĄœ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄø:ĖĄJbbd6bbdębbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdčbbd2:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ•:ĖĄh:ĖĄû:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄˆbbdbbd˜bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd“bbd:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄų:ĖĄ`:ĖĄ2:ĖĄæ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĪ:ĖĄbbd'bbdŌbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdĪbbd#:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄâ:ĖĄ,:ĖĄ :ĖĄˇ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄû:ĖĄ`bbdFbbdâbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdßbbdB:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄą:ĖĄ:ĖĄn:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÅ:ĖĄbbdFbbdÔbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdŅbbdC:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄū:ĖĄg:ĖĄ%:ĖĄß:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄũ:ĖĄy:ĖĄbbd'bbd›bbdíbbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbd˙bbdėbbd˜bbd%:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÚ:ĖĄ :ĖĄ:ĖĄŒ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄč:ĖĄDbbdbbd8bbd‡bbdÁbbdßbbdëbbdëbbdßbbdĀbbd…bbd6bbd:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ„:ĖĄ+:ĖĄß:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĐ:ĖĄ.bbd bbdbbd+bbd+bbdbbd :ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ˙:ĖĄÚ:ĖĄ&:ĖĄy:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄÆ:ĖĄ-:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄü:ĖĄp:ĖĄ:ĖĄŧ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄĐ:ĖĄC:ĖĄ:ĖĄ:ĖĄ5:ĖĄ:ĖĄ:ĖĄ§:ĖĄ˙:ĖĄ´:ĖĄ:ĖĄ9:ĖĄá:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄč:ĖĄw:ĖĄ:ĖĄ:ĖĄ|:ĖĄŅ:ĖĄ':ĖĄ:ĖĄ¨:ĖĄß:ĖĄ2:ĖĄ\:ĖĄđ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄÃ:ĖĄ\:ĖĄ:ĖĄ:ĖĄ^:ĖĄÆ:ĖĄū:ĖĄį:ĖĄ&:ĖĄ:ĖĄ‘:ĖĄT:ĖĄ:ĖĄo:ĖĄô:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄų:ĖĄĖ:ĖĄ„:ĖĄD:ĖĄ:ĖĄ :ĖĄ:ĖĄ:ĖĄ :ĖĄ:ĖĄD:ĖĄ…:ĖĄÎ:ĖĄų:ĖĄ˙:ĖĄ˙:ĖĄæ:ĖĄ&:ĖĄ:ĖĄ#:ĖĄ:ĖĄ:ĖĄp:ĖĄđ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄö:ĖĄŨ:ĖĄÁ:ĖĄĒ:ĖĄž:ĖĄž:ĖĄĒ:ĖĄÁ:ĖĄŨ:ĖĄö:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄæ:ĖĄ&:ĖĄ:ĖĄ]:ĖĄâ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄæ:ĖĄ&:ĖĄ::ĖĄž:ĖĄũ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄæ:ĖĄ&:ĖĄ:ĖĄ|:ĖĄâ:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄæ:ĖĄ&:ĖĄ:ĖĄ-:ĖĄ:ĖĄâ:ĖĄū:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄũ:ĖĄß:ĖĄ‚:ĖĄ :ĖĄ:ĖĄ':ĖĄs:ĖĄŧ:ĖĄę:ĖĄü:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄ˙:ĖĄü:ĖĄį:ĖĄ¸:ĖĄm:ĖĄ#:ĖĄ:ĖĄ :ĖĄ6:ĖĄn:ĖĄŖ:ĖĄĖ:ĖĄį:ĖĄö:ĖĄũ:ĖĄü:ĖĄõ:ĖĄå:ĖĄĘ:ĖĄ :ĖĄj:ĖĄ3:ĖĄ ˙˙ā˙˙˙˙˙˙˙ü?˙˙đ˙˙Ā˙˙€˙˙˙ūøü˙˙?ø˙˙€đ˙˙āđ˙˙đā˙˙øĀ?øüĀ?ĀüĀ€ū€˙˙€ū€ü?ü?€ü?€ø€ø€ø€ø€ø€ø€ø?€ü?€€ü?€ū€˙˙Ā€˙ƒĀĀ˙ƒĀ?đ˙ƒā˙˙˙‡đ˙˙˙đ˙˙˙ø˙˙ߟü˙˙ŋūø˙˙˙˙€˙˙Ā˙˙đ˙˙ø˙˙˙˙˙˙˙ā˙˙django-q2-1.7.4/docs/_static/info.png000066400000000000000000000260551471170400300173310ustar00rootroot00000000000000‰PNG  IHDRÄKøNđsBITÛáOātEXtSoftwareShutterc‚Đ IDATxÚíw|UĮ™÷Ÿ9íöĸ[TŽ$B*š¨6Ń ļ)v0Ø$ąÄ‰—[ė4oœ7‰ßMvY'yw“upâÄ`‚pĨØ`:€$$„Ę•nīíô÷I 0ēē‰:ß ;šsŽæüæ™ß”3ƒŧ΃Á`0L_!p`0 ƒÁfƒÁ`0 6 æō ūpŲšˇmœxҧ˛3§õ¯EÁ„Á`Ž;. æĻDŅņ •ŒKƒÁ`3Á`úŌŌĻâbĀ`0ØL`0˜Ë"ĶąĘAž:A”¨AnŊA>?™!û3Û6fōãĸfū)k–xQŽ“:AŠ,¯1Oí=Ëz¨!“ß‹JÍõUëš8B¤,AÝ­ē4 D•˙âčĐÚė q@p‡JÚüļ‡ÚŠÖ‚Ö¯ĸz)ØD0åMz!Ķ{BrZ3įxȞ“häPZā€5jW <†U•´™ĮŠŽ¯5ˇ¯7k´IGr‚į”Hôđ†ŦÉQäĖiũ@¯]X“f‘:ŋ¯Įf7MũĀiSē„•‚Á`3Á`.?č œâĒ"u3ę ™ĸМéŲ̐5]iiŽė'KÛēŦKršOHÆų' VQ´g9?ŅƚXīžÅžãúŠŅÍŽKˇr\Éļ3´ē÷FZö¤ÉSjr ōZw ޝËyDí\ŸōøŌzN2gHˆæÉLeL”Vɂ=Ãĩ3Ī—q*}ˆĐyQQ؞ĨĶb›Æ"V! @No ŸV§E;< _—&hũZ3vĖ ^€‰ÁôĮP‚ŒāÂŋT“.¨ˆœRC[L1JË*KíiYrŠšPĄCŸ.’Él—17ĨæV dBÉSžļ„ĩ#ŧ E ؘ¨Ę$’Ö0xÕ –PÅ ‚īÂH’¤ŠčGú”–ÔpŠĄnŊāÜô…ŋMTؒVĨTiŒ* "’ÕŽˆ‰g- U¸fJŊ4ym ƒÁ#ĖĀÂkŊoDØķ ­?sY“’î-Š'$š‹5œëœÚ@m ÔÛwčČeš‹JĀ—B"Įn)ėų`xĐЕxtE*…vh„ „DŌ2€ ę9 âÚPef¸E)đ!X ¤îoŖHL˙ĩˇSd2ß­:n#•CDŲmŠ&âÆĄēÆƒÁ`3Á p5Šį×éē–. ’§¨’zäk %‘JßôaėŸd5wÔĒĮhbĩ–PeAđHĀz“F+_n uīÚwO¸t@āōI’"ôi_p[īmTE•%‘‹ū]Î# eXŸ'ģN¤ÁAžÆ g´Ģuō5( › f@AeŠQWšÔãXGk ádäa™|tĘšŌpŽH=×ų¯ŠjÆF5ÃŊžw uõ˜‘ s]3Ą2Á{Ž.\đʘ˜Š.ĩQi>žâ—”#ũÄfKĖĪŗÍ„rjD×üĄ`0˜äscA˛Ú˛˜ÔëkP‹Q†=“åkFŠæ:›ã¯W Q†­ËōˇĨÔä įŦÁz-ĸÅ8Íĩë9VĸôÉ+Ķą>3ÔŽ#ĘøÉ܀ãę6ČĸxF\ŖRbPړãRö:fN§ ˈCX—#āē0<2Á`’#ĶÃĶyŪEaQĻläˆīdĘšv…E‰ä6 c}gS¸›¨ŒĘôGH@ǏjLŖ9ŋŖĩ•įŒ‰ÜĀ–R?ˆô`—ié9v5&)f˜Ņ.ė˛ĩ¯Í:ĄĶ’NåųRÍËj‡Åû´DAƒR‚ÁÜp |j(sSą´­ËĸīëļĪÄÅU›?\j¯ ØîmŸ‰›Åaų2í-ǧ˙øĨP æ†Os`07gã*2Čy›ŧ׈„Æ4QëĶYą“Ā`nDđ4sķ RŦ‹&ô­Ķ´9”ÍgzÉDZ}. ×­´Īƒ(vōŠÖĸšÜˇö2"ĄŽ›éUĀŠH,ål0nÛađ @ezīŸÎ6ˆ`Á÷:n˙ÕÚA{\͕LuO–ĐĶĀųÕUģ­:›íđÖ#uvËf†šDā™cīeī˛ĐķŊn3ĄÍ-RyíšmÚg_[†å֏Č1& ˆ­ ŪnÁ—ŒJō‘ĄŽ—O÷Ž)dÕŠ:´•_˜;:ģ}IuQ—,ÛˇpaP8”ĩų’•oU‰ö!2fūđŖŽŋņ–ōÉgĶąėúŨĮ頏’|ŒÛ+úŽĢ8WŦ PRŌxا+K|SĮDr­CPU™ž¯7õ&Ø7ē°ÕÅķ§[×ŋō^•K‚ĻMĢ×OZĩäūÂ÷_ĢŽcÕ P¨ˆ눆zOŋ ˙Hš3Ķô[ÖdšXYkNØttLæM5ĢKėOۃ6˙ÉvŅ !Îm›ŠĶnېŅ“Ė%žš ėÔ?sŋlīŒīdēŪÍĄ­9Ÿû‘ÂȃH~¯É{põīiš…—ēö3"éŠČEij‚Ŧöųƒ•¯ĩļ É  <7~aÛ8ŅøŲēL'ÚFēī^â ÖØŽúQREuë×äx|0œØgûđˆ‚—oa‰&õÂĒĸo<ŗTũŅ+ožÔ?ˇ‹XwŒ$HÉŊÄÃ>X@\Œj;–^é Ŗĸ0¨Ây÷ŗ.Ömá'G$ēä KHēœ4úr/Ė-ŠēiĘe QvKcL¸'jnᘘ&*ôžJOАĩƒø–S*ã×ԚœĪ›ēV×ßneōæĖ-Ô _}だŽ|cĶģūôĢX~ũƒŋíãzȝî˜Ļ!]~"ąõAQ„1t˙"y4ķŨ-j?‹H“ŲcĄÔĒCÜāÂŖž(Œ2ŠH€Ô˜UķFqã0pfB{.*˜Ųg×_D$!YIËÃY՗dąëÚuĮĒŨ+ûËökúP—]D Ã%ÎEđĢČP‡ÛŽüͨžī…šĩá‚t\•(,ģq/j&]Ĩƒ@ ŅqCÁ*ØL­ÜÚąH‚˛,RÂÁÄRWŦÆüųædžhĶˇ6ë¨ŨŽå›6ūúÅít×÷T>ö“Įu›~ųßGvŧž‹9Fû<”wĢyđ ÷ÜIęˇw+99ą]a€ĨMQ+R|qDãg(=§NY€—Ŋā/lÁ{úLlqYĄ8ë–caYZŦĄÎ‹ß 0åŪq{éŪų‰t+›™-Ķ&íŲ‰e{B,šÎ1ŠZS|ôLŸíüīBūØ`I.kԊ*›3˜UT°ÛË{|ˆ‰\Y!ĢQ‰Z¯ĸ:LŽ~ß4ôĮ¤’¸ÕÂfäÄĘÆûĘLŊíŪĩ#Š˜2>T̍s 0ĩ­RqąđR"×fŦtđs|é‚VĪLv7*ŽT*S %2Ņļ/kk#wĮBĪÕm)Q™ķļ664žíü×ÔĨ¸ŗélŗ';wũ^Ø!Ũö/TšąÎI6 %[ßlDž`hBŖ šĄ3bПwŲ ŪŽ×mŲîŧäÉFåŲ•/|âáÁŽ›ëđú82"§3ÖĂS*‚S ’Ūŗií5L|Ūãm…ĘÎNā’§CČŗ7įŸ‡‘SíũФēˡč[āčŗ•–/”ŽqÕ(SdĘ$–÷¨mąv‚‚3më!nÖĖÖ'î.¤ŪŊ!ķdD6|šũQ…ˇbzÛx€@úÛĩ•gz­‚ŊÜ s‹‡`–ö˛îĶļŗ@¸kT°HĖKJ 2GŪĩ‰3ŧ3 ¨Š95û7XŽ]QˇIĸęļf—´Í[@¯ßdôXĸ˜…ĢĶw7ß5Īwö–ļgąõ)Ā nã§{šŲĶėOĖΧ9´#ƒŸíIĩ‹؈}›kØô›UĖSß|öwËiŪ[ŗãĩWןaąŌPÃøÔP ƒÁ`0W~Ũ ƒÁ`0 6 ƒÁ`°™Ā`0 ƒÍƒÁ`0l&0 ƒÁ`°™Ā`0 s-!­Vë ø­¸‰ß:7[§9ŅDá r0 æ´em/ŽzOjŨέRˇëQ×ÔIū‰åbK•,€š†P@ęĸfúFá”ânMÕkå9‹°ß`˛î\öÔōé%fšsUoũûëëöģņŽŽũm OžÃ_’Ë+ yŌīĐũÜrī§Ū &cō#ĪŽœmúüåüo=—’°û–tÛ!eÍj~häEįv ļŋlVĨ˛_9ëҜŽ&ü^Ŧ6ëõZŲ8ļuÅŦ\B_x×Ō‡î›6,S œĢzûšŋŧŊĮ…ˇ‚@3ŋãaĮH¤Ų÷‘Ĩ%&gŽđÜņ Z—ŗ×į?úeņÃ?û׊ŽĩĢ^8°L^žōųųį˛ŽKē_Ú9E|æ×wÚÎ÷Õ>VVé¸L›ÂûŠ÷cÛĻ=ņĖw&r ą+vߒnWĸúOļč]Ŋ7)N§č­x—a§ëōIø´L:ëÎ%sm-›W­;í„ŧ9ßYųėb-?~§ wåĖLø‡ë¨¯ÖfTē8™tFKŔØŅwĩŦÅ÷č˛pŨšAû<€b'¯h-ĒÉ}k/# š+™ęž8,Ą§ķĢĢv[6P"iíK.@ÚØ¤Ųîō<â鯪ÆĀ­Q!ÔÅķ§[×ŋō^•K‚ĻMĢ×OZĩäūÂ÷_ĢŽcéõC×C“ČPPÕģĶjœĀĄjęÚß:šIiEūiBy‰Ä•Į>Ę<ā$ iRõęø¸™žQœÄRÎãļŋĐKRß.x-̈́6ˇH}äĩįļiŸ}mYĒÂî[Ōm Oú\ xŠ0–øĻމäZ†6 Ēū2}_—6˜<ĮˇF Ēß˙sVS V,yuĐo]1Rˇaˇ='õņ‚×´€ŊW˙î ĻYDĘÂF}JÂÕáĮŨļcé•:* ƒ*œwĪsˇ¯ÎĒpM™ū/P—ŲŸŧĢßîGĻûįMŅښķš)Œ<øˆ›DĸŠö;Tiz"Öîfą“@3!Aœé&’Ā*ŠōŒĄŠ|TõļšÎ… rÄr|DËČŅÜūvĨÔˇ\Ļđ¨LĸvŖŠÖEP'vš ŠÆ[ ëŦ6k îw•¯õÆAcŅ’8z^=2ĢÚšŲ<{nûãŊ†jŨŠjmŖ'…ķÁ)nÔĘT“ĩyˇš•<ÎŪ“’¨—™$$"Ąĸ ˆ†igˇīØSR_/x}Ú6”˛°ĄOIˇou0ú—­ôŸ˙ŠuķāM $Ě GēDY{X?Ž,”Ž‘ëãŨžƒÔŸĶ5Û,§ŧbō&”hŌK_žtņļĪ^¯Ã#Âi&zh“1PƸ c—Ûũ͈@!āú”KÔ˛Z ÁÎĒ"ķ´;Æ[Ą|e<9 $ėÆÍočŌō"Ĩeáģõđ –?5$_e…>Kž/Ŧ|IIÔËËdĶ.Kõ|ĪÃO]gt'Ē 5­”Đy…“úzÁ_Ø}Kē]‰č?Úl8?ôÎ;rYš\Ęŗ 4B€Dĸˆ]Æ&0maÔOšŋÁyT _xĻĸõÍßo憨™ @ÅŠIwv d•AAåT—ųŨŽį˜}ë3ëbô'sDBžĖžŠäĸŋļfNē%ĸŽõDa”IEˆ¤ÆŦ‚˜7ŠĮÚúĩ]#ũį ûÎí Ī]îš;Vĩf?It(ËĐĶ"ĩ$I=ĢĀnĮíA[d؈Đ䣊ēôw>Ņuŧ˜×cR_/xƒ [îSŌí‹@<Ė%k&cčūEōhæģ[Ô~‘&˙˛ĮBũø/ž„.rof‰v9‰Ą ôōƒÂĻ—W}fĮK/B!iŖCĶētD ƒķŲ­ö "’Ŧ¤åŽNgUwé߯ €Ą$B*Øõ/#dčc.1Ŧˆ€ečĘE Í-(ŧ§ĪÄŌĘ DgŒ(,K‹5ÔyąĒ¤´CĒ– hĖ"‘T‡2Ī8#`Éį_ŗ°I’’iž33ŠØu?ÍūûF=Yä/3v‹Í—KēĒ ŪĀÂî[Ļ;´)jEŠĒ#?‹€ŌsęĢ|EŠįęė)ßĖí“Xôâŋ-”ß}ųˇī×Įņ Ø@CfåMŽ)æš %*¸‚IŽiyčô6Km€DH*4R-ŠŲawx†Y$În8ŪBŠ,5‡*ÆÆ‰0IgâōŠÃքŌGrßrą98P^ ûÛ–æ &yÆfJ\ģžęfß´J¸Õ“—Ė/曚ƒŠüŲß||ėøīõ_ųDŦŧūč]YƒSËY#ŠôėrīÔŠyŸŠÖ—LŊ˛D¤Dų„P&AFXÄhšĖÁq ĪøzN’{V/Bū˜p… !Ÿ]*M'OUę=<$Ięã¯KC•×Üឝ_na÷-éļRĶå‡'ĒTą‹[:DsĨ#ãę Ē%€ÔY‘iŗBJÔ~ÂĐŊā)hKxĖPT{XüZé)mĄŅŲÔŠ#6­J–e&=T>˜<ũ•6tq´Ŋ‰$zY'ąøĮ¯9ÁtĖ~ŠsũwŨb”€ŖĪVššÆĩgwžäIņC+ŧ#ĸ€@úÛĩ•Ûͧ¨ĪšmlŌwų Š–Ē´Æ ošÃö/7ũĘ,:cĘŌ§ž9ĢĖLķۚoũīš/x—‰~2æāė9AVAAŠyT5•æĩ ߛzIÖaž;ƇsŒ°õWfVē$MęQŊH(ēË9­8ĄĨÅ=ęĒŨ];ŋ%IęÛ¯KÃt×˙ũÃÃõŋ¸xĶĒ$Âî[Ōm‡”5ĢųĄAúĩ˙0]új(mã]ŗ+b 8ŸæĐzãlģ9w •˜ąÂ>Rҝd¯ŲĨ˜øŧĮÛ •]Čŗ7įŸ‡1iuĐoũöôģ¯g´~Í Ü,ũ:ĖāG˙ßoæ[.ūĐųÁ +×ã&ĖL”–vũW™˙-§ņ„mũn‹Į„0 ƒÁ¤Öéčv6Oˇøōéž"%sļ‰ÁŨg ƒÁ`0Wh&ņ>Mƒ_Ō ę'>žƒÁ`0L*t›æĀ`0 ƒšrđy^ ƒÁ`°™Ā`0 ƒÍƒÁ`0˜›”‹`ĻžÉˇâ;.uŊĄ%vŗ‘ÜÄo›­Ķœčmg*uqûˇuMäŸX.ļTiÂxi(f€@¨G¤ĢŸíŸŨŌú§Ļ`0ļŦíņÅQīImBˆé Жđä;ü%šŧ’™'ũÍŅĪ-'}7Pė‹ÕfŊ^+ĮļŽ˜pķ4“{īĪ˙sYņ™?<ūŗaŧ‹G åÜĶŧ¨ôkûŲ%ôV[ÛŽÛÆ4˛aŒũŅąŠMoZÛoōÍqT#_øëKã—”îÁ_>ūû“É_¤ÍŖį<ŧhք˛ 5‚„ķÔÎõ]ŗĢëĐ<ŠūŽW,—§#¤PķĄÍ˙XũÁŠĐmŊ%!! ¸iV†rÛęėS‘ž—">oYāôÚŦËxÅz4§Ģ ˙­´Ŗ“uឧ–O/1͜Ģzëß__ˇßŌŸ‡˜ŒÉ<ģrļéķ—/Ū¨ “1~ŅŌgŽjV[ũ‡§^ŨsūļrúÂģ–>tß´a™jā\ÕÛ×üåí=Ž[rã*>s‰kˆ;mįûj+Ģt\ĻM %nÄ^ÔÍÜēŊtŸŌžĀ;šögŠ:÷ÚÖ•dĶĮœ<õg|€Hú¯c׊fĮŒå<•Λ?(ŗgŪüņO7‘įã¨ēôą–S'É^$5CFĀņ˙ų÷FW\•?sÅĶ+_⛟{ë,@č+ž÷Ō˛A‡ūôVU… Ŗøá÷~ôũö•ŋŨē}‘.:"X™™/œ>N h”Ŗ-‘l|ē‡TŪeØéē•ŠVYüđĪūuĒcíĒ,“—¯|ūEūųŸŦkėĨG Û´'žųÎDŽ!öõÖ2ãîŸüzšf˙š˙ųÅ5ú!t)ˇį\t֝KæÚZ6¯ZwÚ ysžŗōŲÅZ~üέ¸ '•ČPPÕģĶjœĀĄj:ĶŊ`Ĩ´"˙´ Ą<‹DbƒĘcepvŽŗ`l{ī ˜DāéÆÃéÛ)cĸš’Šî‰Ãz8ŋējˇõ`ÕҊ"mlŌlwyž@ņtãQcāüųbVßŖËÂukíķ Šŧĸĩ¨&ˇsķã$Ožį{ęø¸™žQœÄRÎãļ†ëŌĖP3øƒQĮßxKųäŗéØô|˜q…@†žôēî ĪW6–øĻމäZ†6 Ēū2}ßjC‘í[¸0(ĘÚ|HÉĘ)åŌ õ•ŌšOjčîÍcO5…PĮ˧{Į˛jEÚĘ/ĖĮ¤Ü[uĐo]1Rˇaˇ)jC™ã}đÁpbŸíÃ# ^N-É ¯HÄǞ[ŨŋH5…âÆ/l'?[—éāDÛH÷ŨKÄÛQīã‚dēŪÍĄ­9Ÿû‘Âȃoā×Q3Cî} Čˇí“ŅÔ;Ī„&gÂËJåŗkë;N˛â•‡œ÷‘ū~ŗglåcôö=ûˇņQ ¤P8œK4ZëNćB…Fũa? \ĮuĄ‘ácŲķ7 ŊIDATᖴĸc¸kxŠ<Ą÷HĖ›éœbÔoøSŽãÛ'đ–ĩī|IKQp˜7ũÕŦ.ą?yÚü'Û%Ķ\SæŸ˙ Ôeö'īēÔfËëßĘĢÛ<÷žĀžˇŊ‘îX1%h=iqHIbī m.ÉWųÔw6ڒŋîT@S^lĻĒ[“ö(%īÁÕŋ;¤iÖĨ‰Ē`Ö0¨]—ųČO~?)O“h?ūŲē7ˇœî˜¤ë9×Å5B•Ļ'bínö–tĪ”jįfķėší+Ēu§Ēĩ˛3ZPܨ‰1Š&kķnuĮQįEYÛ÷¤ÕHIá*Sí8{N–@ļ0` UäŖĒˇÍu.‘#–ã#ZFŽæöˇ+%SxT&QģŅTë"¨;ÍÅNãÕÔĞīŃL’PŅDôķē/ašüäsĶíĢôQ3ĢAāwgŽ!ąfÑ.•×֏+ Ĩkäú8‚Ū´!  Ėõ.x ŲiûødįĄw‚ĸ9QzåžĒKO§ģlM!mū1Væā›Ļz? ęöϧåˇLŸ8šUÕk¤Aˆ¨Ųf9åEs\‹JF/š­?ũƎ>5ŲĪúų~PĖÄĪí~ķ§ommë,Ž~ïūôøË¯ūrč—mŲãô‡^ũÕ{įnã}ûÉ´đ0yn;skĪ%BeÅüŅŒ øô'}ō‘ė^‡Š`2C…*Ļō-Åæ‰î=z' (Ģ+*ϞĘū‰úúT2bj/Kě)ÉŦ:į'“PpjH}’Ø{C@¨Íˆ{Ο*FŊqĐX´$@jÃĶčSO‡Ø”j˂ō÷˙ņ‡ŸļHyŗž÷ĖK/p/üâc—˜$×E[_žtņļĪ^¯‹ß’rĻ a7n~C—–)- ßũ¨‡o°|øŠÁÅbø,=xžTôpčétŸßr% 讏‘0vŲŲąŨ7"PD-ĢĘė,m™§Ũ¸3‘ä^ŧL6í˛TĪ÷<üTĐuFwĸĘPĶJ ×zŲ3øžįžkÛõëWøđ(×|Yš\Ęŗ 4B€DĸēĖ\rm Їæ?ÖÕÚ6]pŊæ@BQEjmõŅTjŠŦÉāVáˆt‘js385Rõ>\'0mák¸°‰Ęšĩh ˇ˙WüŠéX ė˙ãķ5j•>ŊhĘ} žz,ņīŲŲÎiģpaÁŲĩ|c<=ņĐŌšĩ˙ąņdđ6­˛Š$l`Õ;ÜpŠSdiY$­ŌäDĒļJ1qrČļKÕĉļQQ…Ã|&Ôņā@–Q‡ôd Éō*O $ ‰Ŗ8@F2ĸ—Ø{Ŗo˙~RŠWBŦrõīߊŠÉmV˜úĢ9㭟}äHeČŠ žđLEë›/žßČۚzĻ:;]ūs†}į ‡ö†į.wÍĢZŗŸ‘d’,{”Ų"D€€Ųˇ>ŗŽÛģp2G$d ŋĻyŠį§MĨŌ‰īų^ t;ÖhÚ"ÃF„&/TÔĨŋķ‰îšžVĘä͙[¨5žúÆ>\ųÆĻ'v˙üé?V'p{?ũcčūEōhæģ[Ô~‘&˙˛ĮBįS“jCÖâ[NЌ#\Skr>o"å”r•—EŨŽāäÔjĘUĶ‹Ģƒ,!éڅl¤)ž~NûĮŋ¯‰ĨzS)æsÄ|`omŦ=ĮŋúĮ'žyāĐĢGb„ĄâŠīV´žöôÖϘ ģW˙6úėĒįŸj\ųÛĘÛrŅŕ–  /üA¸ëŖp‰9m  pŊĄíĪČąĨ=1jˆÜ˛MĶ9O/Đõ­ÄܑĄô3i.™-Å&ΙŧˇėŧS‘¯ öŪˆQOF™T$€@jĖ*ˆyŖ}Vš$Äy9ętõ Đ#$7i¨Î$wCūčå…M/¯úĖ~ËNëQwxBĒ– äšE@ägJķ9EũœH.øUd(‰Ãmä%™Ä°"Ą,ƒ\B€(Áĸ9Ÿ†$$+ib8ĢúĒîuž&DėēƒvŨąj÷ŠÅū˛ũڃ×ō•Wžiã¯_Ü~ŪC) ûÉãēMŋüīŖ;>’u€ĄMQ+R|qDãg(=§FŠj#Vcū|s2a_´Āé[›u´ģfzʅÄŧА˛ÉZíOQ`(ęP° 6S+ˇvd!…,‹”p01šÕaĀ|™yŌâŠĘ˙ŗÍņõ&‹Đ =Úė=VÕÔSFˆ ­QP€2Éfbģũ}W™õ:bĒŠųiämi&(k¸XK6lÍ<ÔĩĖvÂũî’aė—RŖęcįāžQņ4*”#Š?>GžŸmkܞ~æ1Į7ž`yÂß`ū`WĒņY–ČĐ/°÷Ø{Ŋŧ§ĪÄ—ˆŗn €0–ĨÅęŧü•¨÷ĸpîŠmbŽ.M{ģÉ)cÍ7A -ØĢ‘CĒ‚E/žü ôîËŋû >~ o @ī˜,’ČLįŦYąaĶ\“ŦDS #€ĀTUĒč2įŧÉáœtΒĪ*4öRb@ŋī zcRIÜja3rbeã}e&Dŋî„S,šé+J´ÆDé4īāŽŲ^1ĸlOˆEÃ9FQkŠžéŗĨ02‘ä^@ųcƒ%šŦQ+ĒtlÎ`V!PÁküĘĢĖy[Īvūkj‹ˆRÜŲtļŲ“ĀŗŨ/‰("Ā Mh”ĸ!74cFėÂ~ ŠhC&ÚöemmäîXčĸę=aˆT !NP§ÜwŽÍXéā+æø Ō­ž-˜ėoTŠTō}­„"oî’âČÎ÷_fų93tŲ_z.Ëg. c”.\1ꨥšfkNņÔĨßthüČg5€wŸ8MŸŗdö›)͒7~ÁŠ{ĶũGŨž+0ĨôáauTs¸Vér*\N…ËŽ=|šÔ…ĶéNĶĐZĨ˛ƒcĘYŽŪhŋ0)ëKũ…ŧáã ļŗļWj‚)bbWVČjTĸÖĀ̍̍_Ibī BŧnËvĮā%O>0*Ī6¨|ávíØ\KĒŪ¤ÄNŋģŨ9dé™0$3Ŗpú˛īL%xÔ'öæ$˙ø•GŌ­}ŋ†T\Z\RZ\4ØĖ܊{ØQ Í ” ˜Guō“ė fĢ2× ž;Æ{M€ ¨ŋúP×[!>Íū¨Â[1Ŋmŧ @ ũíÚʎ×MEúčæ,å÷ėeAR¤ZĒŌž´{Ë;#Ģjī‡&Õ]žEßōGŸ­´|Ątë|äņyˇv.1â–<@žŊ9˙<ĈIî%eŠL™äŅŌ€âõĄ-ÖēndoˇņĶŊÜėiö'fįĶڑÁĪöœ°MIUˇ5͏¤mŪzũ&ŖGL’KĘį7šL9¯¤Í™#īÚÄŪÔŜšũ,Į:Æ9’T‡kŨŲЗ/ēÛŌŧasũåĻåÄPsk ˜Öæđ…ˆ*&Äô‰Ë_\jR€j>šã÷˙öÎŋrėä_~ũ:ˇâÁ˙Į %@ĖQĩõ¯ŦĢģ-'üvDĄŦŌw{™™pŸĐG‡GdšÛZ`Û gXûˆ,˛j'Ķ}(SŒQŧ>8īÁ. Ŧ|/sŋŊ÷Á™ļõ7kfëwRīېy2„€JĖXaŠīü~ĐÁ#Ųkv1}Œķ7 \ÃĻßŦbžúæŗŋ[Nķۚ¯Ŋēū ۋz“žyį•WÅī~ķ{˙ū "ÍûŪzõ/ģ{[ GgLēģ€˜øÄKĪčüā…•ëoŊ&đä s“ Ės-ģ—?önæ1')Ę@*¸’šö™*ķßÖë#xŧs=Á/+b0Ė́d*Šjŧ†SNRėXfÎŌN7H™Ä[ôc°™Ā`0L  ˆ“‘-Ą’L‘D€hŅZčŸQ.zjUl&0×[œxšƒÁ`nHĄ`ŠkōđxšPܧ8{Ü´¯JÃsl&0 ƒÁÜĖāi ƒÁ`0ØL`0 ƒš~üĖ]]dÛw,ŧIENDŽB`‚django-q2-1.7.4/docs/_static/monitor.png000066400000000000000000000222121471170400300200540ustar00rootroot00000000000000‰PNG  IHDR„bFWUŽsBITÛáOātEXtSoftwareShutterc‚Đ IDATxÚíŨw|ÕöđsīĖlɖô„$„P¤ˇAņŌTT•&*ˆJ}ā OÅ.TŠøÄ'*†&"ø¤L ŠR6eŗŊĖĖũũ‘Dĸ˛b Érž?ÉdæĖÜ9įŪģŗ{ÉáÇį͛gĩZ!„BMâ)@!„š_ũŋŠŠ <!„PScˆO4úd„'_ ŊävŒãÅx1^ŒãmņNS#„BÍhdŒŽ)ûžĐ˙[Æi8<>Í)›/{€tL:ĩÆ3‚ē–ÅX.•,•ƒ=ã¤<éĮ—娅á Ų›”#í{õX͇Ģ[Ú)dpáu÷‰suļÄņ#P%iņC6ÉY›ĨėTæa Œ¤‘Ŗ¸Žũ mĐõjחąüW=ŋæÔü$DĐļ㸎ŨÉUĪŠhŋw`‘˙ˇ@jy—ü¯šZ[~øBĒrŗÜ-búæĐ÷åzOæüU-ž‘3“üķ?DSõŅ÷ ]&raūāēÁâpgJŋ})å1Y }¸>Pm ¯1ŗüŋįEÕ\Eß.bϏw5ëōšĐ&GÆž‡@čŖÂm(ßä9-sˇLĄ‚š(Z~%‰åŧ+žŖ\ĪyT¯dÖķŦ¤ ˜¯_LŽ+?đABŦä{ņė;LšRhz#6f¨øÜ“æán™Byĸ&J€ōmbj é2‡Ąræ:éČdČ*_ˆēÕ#B—Xđ”Ęį6JĮ>&CįQ5šÁâĨ åIŋŦ–Č­|˙Ʉˇ°âãĖ%KDË.ÆN–ŗYĖ8ÆÜ ô}š^“šuíöMbÆqæf ˆ íĻķĸ@.’ö'J686×}ąŪrŊFœj™øˇ!BNYúĮŌš F#hģN-­Gia…$fÕˆ>"̇ËŪ¯—ũ¨”’$•L‰í1‘ ôĢ÷úŠŦčéôO˛ŨĒ\ˇĮ¸ČĻNS~  <Ũžøyą0›ĩ %õĩįKnoÉĒ3¸üÜuÚŗCÎ9Ęô÷ņíē\÷ąōŪĪåōh¸Ÿ/äA!„č""šÎ…rņ^ŲîĄjų‹WÁŠ˙+YÛōwL¤j $°›O—>™e¯đœâüķ%Ŗ„8Ž×\˜ŪĮŠ1ƒ˛/Äß2HĪ|a™ŠGļ’ĄR€õ'1-ƒôZćhĀv^ŽūžÁ ũˆkÁĶÔŪ/váņœ•KXFũФ“ë˜Ū’Ÿ āXec _§cTĪõ-4‚īԖ(<,ī3ņč2tŧŊžAÅ6ņdé6[ c’xę=Đ-ætÍŖ'Nxā(H"0å—lĪāĩûfî21ŗ‚ېęøŅTå‘++Ā7ŠSmŒō…SŒFq~Č%7Vŧāb΃ū~ĸē‘ęu$ŗö/(ú†ąÂužȐ9´ ÃįûSŒåŊęÎĢ;ũ.9÷$ ™ĖĮĤË8ZøŠ\9ž†ŠÁ] ˜ mCTÔĄ\ˆO_ffešéĐz.КīzÜ}´¤E Ą]ļz’ö’Đ.´U?ĶíUų¯ôwpĩ=KŌ~ÉŪÆė"ø ^^m—3čš|l{€˜ûš‚ÅR‘‘ĶE4ƒkįfĨ;Ĩ2™tŒ%Ä[{&^ÛšoļgķQÖžiJ”Ās:|#60ŧá6Tˇú~ÜĀ TMAēÁâep¸AJ|´3ée† 7Ž$b$wf•TfĨQM78ūģÐđG„ŽQr‘tl=f››„„×\TĄQydĢÂÔ Či_L¤­:‘āž4ē á|÷Ę3s0R3īJ@GhI A7„֗•ĨĘÆŗrÖZéü­üāÉ´žˇÃ=ŲŌo[å šLŽųûLöŪĶ“+™ÕÅJWÕíˁÎÖÄ!‹ĮŤã՗Œ„?ŽVqéöJŊļķ`ĖG" {XčÍLĨÔdæ ãŊ1¨ĸjžĐ¤ūDÅĀlhŠÅ˜e h áĢéwyûĮÚsƒß Æؘ&Ÿ~K*œ$ L|{R„Ö–.Ō2?ŅDu$l 6ëĐ_Ü÷ŽT<šÆx{ Ņ)§Ŋ#™úōƒŸĻZ-ˆŋŠģ7ÖûĀ ¤ûKBÛāf/וŋå~Âķ  !_}˜Ė{7ĸ&0—ŗ6xˆ@TžŌķP„}ĸOŦg<§÷°Vãȍ/§•FÆĀ§Į„ųË]KkĢ“.noōžVã—Bĸ!~ VU\šXœŅj/&÷Vƒ¸nO 7„ĘãLüÁ0YöĄF@ÔĖæÚRUČZtp| Ɯ˛Åbmâbš‹wˇ#‡‰ @*“tūžUŒAIc{CŲ6Š ›ŲräĶÛdž TTí•2Čf#ŗįˆ PÆ\.RQÉėÂiæņ€ÜŌR˜§”™ō˜Íā`UyŦĒŅ’Ø›Ø…Ũ˛CšTÎ:ŲŌąSNų—˜ūŗ\–ĮĒ2åŗ›${ ôzŊ¨–hxVšÅdŠP>ŗŸũi„ũįëëG; "%ŸI™ŋ2›‘Uœ’R?’*\Íī•~“ŌO0[ĄœõĨ,uäBuM:āšƒ ™ tÛ(ž~ÕãĐÅsũ'ÔpxˇlĀŨhņęÛrŸßļ‰G~&€x.Ōž.’čéŧ}Ŗ”ēJ4]šžP? hnŖžž}E ėĀ%LĨMû$99tčĐüųķķķķņ‹Č1^ŒãŊĸI““âÁYčtžgü5æãÅö|MČ,{…'īađ°ëúÜR= Eā×Ģ „ŽŽ*žŋE’Ęn”ÁÜhņĸ&Å!tÕ´ũ8-Ƌc„B¨(‰KTÄ5§#ēøž1^„B¨iēx B!,Æ!„c„Ba1F!„°#„B‹1B!„Å!„BXŒB!,Æ!„ÂbŒBa1F!„c„B‹1B!„°#„BXŒB!„Å!„ÂbŒB!,Æ!„ãFĨˆzæD~Ō}Qž|Ÿs›ŗßŒr?ęũˇĶËÔØũCĄF.ƚîžųïųCAęūw&õÖ]Éߑígvlû!ßÎZÜš"ęøyo~ųĶÉ\ƒÁ°û™XRŗ9zV’ár7Ü­—€)bGŦØq<×`Č;šķåQm51{Û^ķ[eįĮ’ CŌŦčkSČ…åŊ;;ęn”:‰}—†*û •pe™*‰ųÚ-Äx6lfé/Ž }"%Ø=X>{‘qibŲ‚YļaígÔß2âņ˛ųˌKËÍąę OŽõß§oK9N/¯÷’Ν6˙ƒ˙€­uŗ˙ŲO ×Ŋ]%[× Ëũ÷##GNz˙ėm¯}ž<>➁Xągųŧ5ÉrË+ÆŧFíLŲúæę_Ēę^éâ­ŗGÜYëgxä#[NX)€"ú‰MëÆÛž|l萇7Į~´eVŦĀûöjǘŠkž į×'&‘YĖ”–ęļ(ņęhôʒ!dĐē}.Ą… °ŪKĀí;}‹POø˙.āŗ šŊlũ&ŲVˇj=§ßŊ%đ“õÚī˛ė=˛ ōéÚT˙}Šņļ”ãôöúKįķĢÍ˙Õ\;g­ŽŖÆ,ũĩŠŗáÅXÖöŸ6Æ?įEīīOK;øŅ ¯dhî}b ž:×ęûoĖĪxûŽ1+ž?‘k0œÚ>5R ū}AuīáOĶÔÚ^×ū˜R`0~ûéŊ‡ûT°š¨û’ō“WÍ^šũTĻÁŊÕøčĻâ”-_Z¸ōƒ-?ę뉧<ûtjĩ”\ŨĀ^îŖŸ.‘)ģ<øplųŋžĩ?#ķā{‹ß/Žž8ž›ŠyÛ^Ŋ7žãŦˇ'¤Ŧ\“jž.E‹Čā0ņ4D 癕ũg–ųQƅˆ´Šē%”0ÖÚ}™1>ŅØãū*ŊōbŠŸdé?ĻJ?ĘÖmŠ1~™ąG?ũcŗRŒ˛%Ė)ÕĘ Ũ$Æ'ã7םĻĻD=Ũ”0Ú9ÕÔ+Ҙ0ŗ<"@ĒŠ™RÕ'Ҙ0Ŗ"|¤ŊīĖ2?ÚLk8 ŸN°oÚ§ˇųJ'ƒ¸ŗ>ûúh𐗝NNR¤1WĮvĀ1Mš_~R¤ã Tiû)Ol4PŸ,ÖŸbŧÍõ8yMhDD¨F¸üë/ĪŊí§Ū×3wYFZu ø-=ŋĄš áåīÛ]kLų­RĖ§O^Puíßú÷K5#—=RõʃŨã:Ū2ëŖŗ.ĀĩįŽč¨¸›ß(ūè˜ø÷ZžuՀSĢī4pôĢG^Ųē"Ąv„MÃFĮį/ŧšSû;W>´rQo˙æ=Á§ŋõÉ;aįĮ‡, ô]DÚÎ*t¸KŽĻ˜Zõīęī};Svž˛ú!ķĢ/˙PqũZ,æ!Pâ❗$Ķ($É%ŧD€aíë,[ē.Đ-ueęĖ|HÜ1Nv~UpōÛÁ†Rá÷Ȇ8ēŨä(ø4ĀhĨĀeđ'_ =š_w‰ŲŽēCķëKĄé>f¨EBT÷XÚiÅüSökõ öfû.3ãŲc+]{ô'ėžúN8JŽŗWšî”ƒģxÚ B^!Č|•÷ûãmļĮɧ|}üø7S;ũ>wŨ ¸.ąŸz{°ēÉßg †ŗ?nž? ‚ohũhxåŅ…ûƒšÔZ“`e[ŠüÃuuvXüī…oũœmqÛ %.÷6tgšžŨĨLYš|ĶÉÜŧS[VŽHæī|ŦŸļfˇÎ]k6§ģdgÖîm9BˇøĄ÷LšČÛīĮv­?l&@u`.˛GŪųYFææŅ­EUŠįŧmPÅL[;ˇbų ģÍŌuk\ē´ræ}ʒ/4{>Åx[Ōq^û¸ÜĨI¯/yrĘØ1Ÿú¸øĻgŋØúdTÞĻā¯ŲIcî´cĪå;Î$ ]œē8õŧ]`öė” ŠøŽtXUžÉ ģmĻÔǚņ„DyĒwŅŽžąüņ4XJ Påb>=ÚNÛĪX;Ŋtí=; Ō5ŧ*—PEœ*&„‰ęîŧČwސljÉQÉÉjŲOíåÕOī+į<øųÉāĒŊEŧSúķõ•ÂäŽaVžŒØmWÔÜYeíūŨ”ņŒu˛šČ–Ęšsāš ČĄÎæxšCŧZˇZ*ûæeMWķøÚ#ō9ŋweUÚgŠĩÚÍ2â~á–bÕÁJđmŪî_ŒˇY'qŸyPÔûäīÄåu?—ā8˙Ų[įĢ˙x<97ôøŽÉ÷wúøÍT÷UO5<íË֒*Їi)€ TχĒbËīƒæą]áÃ:ÄûQ3ÆęîŖ9Ή)ŖĮ=ÚŗhĶâ3žęã”-E&ĐGhĘ~~~čPáæ)ū`*2K˛|éíŠø ĄŨ,¯ŨãĸÃ9÷>Û{ä—Ļk:đ"fęäAĶÎÅr.‰'ZŗššËŸm7üõú‘o”Æ™; ˛žúQwŲ!>ką9Ž ķtŌʝž+y¸vËøįJ}ø|˛Ÿ ”eU\åÄ1Ę܍~?ë^#ę(§ā‹ ´ˇŽ¨>ōsûæLĩˇûˇyg!ߏ÷jķúÆeĪ?yÆÄ†ЀG:Ū§‹Nœļ…öėČpúŽņ‘ÎĶG ¯z†•UËv†÷hWũņTĸŽëéÎÍĒla Š{LßĒč‹mg]ĩר|æČMį‘ÕOP‡õëPz,­Ęëvׁ顊1lÎ>d­›4|Ú.ķĩ>ÄEŦnÔŪe+æåRŪįŅI`wPâ õ k⠖Ô"Ø/÷æ(­ā*ō…Âīü]ƒą yĘZ¨ƒQ]íĮ„øOķwJįTķŪj5˙­VķßjõÜ­ģ> yí´Ę*1SD›ÆOR˜žÔ&å2oņPŧŠß0zģ1Ūf}œ‚.<::\'üŨ¸ū˛Ÿ+ĸnŨ+*rĘ< ‰ėoŧgl=ēū[kÛį^~jp׎ˇN_šđ&ûˇũbžęîąžøt§ģį’ÄI}ÚÄôš°dIy÷§ĮŦÍ1Mt§î=ēwVMD§=ēĩ ­}s@ÕgúCaЎ܋ܙ/6å†L{yöāoņŌĖpÃæĪ͜ÄÛvf+8—Y#+ßėfcvVåšįv"ĢS*dS%O*ŠU…8D nV’ŽV ˇĩŠō­Å¨a!CYåžĸëB3…ŒĶ~ÁcÍūW˙QEb#Î+ĩ#­ÁaĸĐŅĶyæ.æ"…ĨŧĄ”7”ō…•ŧ‡ŠŒ+uĩøkƅVŽ{ŒhŠšÅ°ÖbD¤Ē’žyô`W—8OëgĪÖ!ƜSÄãŗßáâí>Åx›ķq2E‡ÉÛūīŖŋ?xUĪëëÉįŨ×ׇŒZúúŧÉ#õK8jÎģÕd}öUzƒĻ‹ūÆģ“Ŧęԋã˙Ąú×ÂÍģBåšoLHŊ]ŗûķá§Š›Û}Šņ6đ8ŊžŪk>ŋšüŋĘhsÅMX<~žžTĻí}m܂u… ûÖrčĐĄųķįįįįcBÍŅ0{˙įŠAn†į!äŗđ›‚QķœEˆa\J%#áRTO‡ķ”Úƒ•!äĶx<¨Ųcøßc‹Öš‰öÃęôĶjŦÅ!,Æ]W4‡ĪZ„į!tå=<!„c„B‹qÃpQ÷%åŸx&ę¯jLjzæDūŸVyBÍĶmÎ~3Ęũ¨÷ßÖ]ĩ !„ĐUcq÷ŽßWNÎ?ģŨ Cƒ…ëw„˛ũĖŽm?äۛūžË,F͔K2 Iŗĸ zV’ár7Ü­—ëŲĪõ_ܛ)HäÂōŪu7JÄ>‹KC•ũ¯—pe™*ɇžÅâZūĮ–ŖƒÁyü?‹ĮDûĘDéč5ļüéĨÆĨ‰åķω7)¨ŋeÄãeķ——&–-šcÕ>­;jˆiĘėŌK§ŗĀŋ,Į‡—?žÂ¸t đÕAš,˙\—ãlŦ|ëu?•Žtœ"˙4sôĄwÜûԛųˇÎØđŅ´˜ëöapąbĪōyk’+šūF¨1jUĖÔ5Oķŋ_+Ū:{ĝĩxö€G>˛å„•zßOS,î-2‹™ōÁRŨv@%ŪBbc˙[Bö­ī|\˜šįŨ÷Ÿnõßywôí;äé-ú™kßÛÖĘ1uŪôõîÖ~É_~˛A—t ė5íŸZĪéwo üdŊöģ,{‡lC|ä!;ĻPNj\ōģ[ß1Jn€YSäŸëqœ•oŊė§ņōĀ•cŠ*/+3#ũdŌΝ8Lû×TWcxÛėͧr †œũĢÆG×îMÛkâÚS Ão?Ŋ÷pŨī>fČ _ūœi0rN}ĩdl4P3ŨŧjöĘí§2 †ė:ûQ˙ž zLYwš:āŽ'ÎíyСbĘė]š Ųæŋ.["†v÷ÎÕ,ežūéē&É?×ā8yMhDD¨F¸\Ūn´ũ4^¸úwđÉú[Ÿŧv~|ČRß%jĸÅŊÅb%NūzIŌ1‚qA’\ÂKø֎ąÎ˛õŠëŅRįQĄÎ„ˆÔÉãdįW'ŋl(Ŋ¸Đ4#Lâčv“ŖāĶŖ•—ÁŸ|1ôä~Ũ%f7ē:čͯ/…Ļ;ø˜ĄQŨci§ķ? LŲ¯Õ'؛įģĖęžtĐ>ęŽÎBün6:Ęöķ÷g[ūw3u´PÎ~åŗ—-zŪ<~ ĶüŠŲR1¸‹§Ŋ ä‚/&0>Ü<ē‡îĮjį|Z埯=NĻč8åëãĮŋ™Úé˛å°‘ö͈yāęQˇ4kŪíÜšũ)5Ģ ‰—ŋôujfęŽÖį Ũâ#Æ4}ģK™˛rųĻ“šy§ļŦ\‘ĖßųX?­Ė†<ķôŨŧ%ËĪ>öÕKĢŽÆN¸7ļfĀëÜĩfsēKvfíŪ–SŊŸzîûĄÄ§žŒ{杭}ŽË73°5éļ*fÚÚšË_Ømžô’U\äí÷cģÖŽwĻZÜ[*$-SÉ:3š`æõ’Z#Ił¤ ­ē8Ũ{4ÅÁSČîÕy:ģüĪ4g!š5e&Z‘{ąÍōƒœŨ{ڊ> (1_ūāštEQŠ"˜SÔr„¨äͰČv.ëNmY1īIWdg¨šiō’Ë-˜đ˙ĘŽŒ‚‚ĖŊoh×<üüAcËWQV0!Čvk+ũž ū›~p ˇÜ߅Ôä jīûŦqéŌʙ÷)KžĐė5útulũÆņö4UūiĒãl´x/\éÜļpī÷é÷V—Âüƒ˙|ō_Õ+ĒŗōŒ€d¯pČJ­Š h§.N=o—Ģ gvĘE|Į@ēO“&<öëœ1÷j, ¨žËYUžÉ ģmĻÔĒęī$°Ē__›ķÉO[gĐO™|´ĸIģį´ũŒĩĶK×ŪŗĶ ]úl’č1Oõ.ÚņĪ3–+ę_÷ÅŊ̈Sń0Q]ųÎ’M-9*9Y-û ˛Ŋœ¯>VÎ9xđķ“á÷…‰Šx§ôį3/…ÉÃŦ|ąÛލYŗĘÚũģ)ã% ëd5‘-•5įŌsAC›åÂM\ĢÛ_Û´ĐīŖgĮnOƒncūšęŗ5ŲCgė.Ąe#4‡ˇ év€"Ũžîæ‰}eŋĶÄJdUÚgŠĩÚÍ2â~á–bÕÁJ_-NbČmÎūA2}z1Œfõ8‰ûĖûƒĸŪ¸ŌåŠ˙ū~/\i1fžŗōŦÕ]UœÁtą§Č˜\ˇ@.Ū͗ŧÉÅ_Wēû߂ÆūđžĖåĪ#Ņ/>€ôé×ZØWՔųOŅqDBhˇ„ÃËkˇ,:œsīŗŊG~i’”ŅãíY´iņOũq5ÕâŪÄLōsûf­ō„tfÚāō9ËOIes–|ģJHqûdŧM•šę8-ŪÆËW:M-šÎ§Ÿ=›~žn%ö6j=—í īŅŽúãĻD×3Ō›U)‹Ndˆ†t×ūũˇ™ĒÛÔˇŸŽŨ<ūõaOŊ3Ŗ_Sftׁ顊1lÎ>d­›4|ÚŽši|Ļî1y|Ģĸ/ļu]î7ŅâŪÄEŦnÔŪe+æåRŪįŅI`wPâ õ kŽ;XR‹`ŋܸ´‚ĢČ ŋLâ¤īIDATķw rÄF4$mQ u0Ē k{]ÍtÉ\ĸÔûq;ŽL’dĸöWĩøQSg ąR°ē&gų°ķ/ķn”¯bÄgŸkReoúāŊę˙´_g”nų€OwûjŧM•û8]xtt¸N¸^ûiÄ<ĐøšƒXO|ēĶŨsIâ¤>mbzMX˛¤ŧûĶcVJLûßüÜyËęwŸŅ%ļM§>#Ÿ\ņū‚ ŠŖ~íg~°8āãŲ¯ū|xõœw•Ī}0ˇŗöēôŪ.š¸4ŗœËŦ‘•ov3‡1;ĢĀR“ŧT}Ļ?–ēqģÁ}šũ4ÕâŪD$V;§TČĻJžTRĢ qˆ@ÜŦ$]­nkåZ‹QÃ,B†˛Ę}E †f §ũ‚Įšũ¯ūŠBb#Î+ĩ#­ÁaĸĐŅĶyæ.ščĀîüāģ—ÍŊ§Gll÷ģf-ģ7ėž ´pD,TĨÚŦˇãBÄđnÖ;Ú) '¨Jú>æŅƒ]]â<­cœ=GXG„(sN/|‡ SŠ‘R¨!JąU¤ w%g4Öügr1ææĘK‰ËgkqS埯=NĻč0yÛáÃ˙}´îƒW ȡWžŸFĖ×ās‘Ė”ŧlâÂWßXđí/¯@UöŽ%^8ZÉœēbôûĒĨoîyZî˛3GžūW‘÷q6SözîāŽg[W˙ôΑŧwNŋ1häģúį>œCŋŋú 8ōÖÜĪGoûpņŪá/œ¸æos\íbÔ@‚ĻN˙]Á~šhqo™Ų+8ŽP›‹Sean‰€¸S“yˇ3ÍÔ@<ŖHOŌ]éR†Œ9wi gģ: ˛&ÔÎŽˆõ—@č˛ÔĀĘÖ噯÷[]ö¸Ē63*Š‘’ũ‚âšaRHygō|˙UΝNzF ÎŌãŸ.œü¯˜ŗõh~bQ­˜0‹P"gŋūÛ3Lâą0uķŨƒAIˆTĨ>Ŋ]ŗûœoŧŸęޏĶ<Ĩ}õŸM<PōŪ‡¤n0M”Žũq6Vžõ˛ŸFËäĐĄCķįĪĪĪĪ„šĄaöūÎSƒÜ¸Œ"BČwá7Ŗf‡Eˆa\J%#áRTO‡ķ”Úƒ•!äĶp=cÔüŠą üīąEkÍDûauúi5Öb„c„Ž+šÃg­Âķ€ēōž„B‹1B!„Å!„BXŒB!,Æ!„ÂbŒBa1F!„c„B‹1B!„°#„BXŒB!„Å!„ÂbŒB!,Æ!„c„Ba1F!„°#„B‹1B!„Å!„BXŒB!,Æ!„ÂbŒBa1F!„c„B‹1B!„°#„BXŒB!ÔxūĪ =X}ŒIENDŽB`‚django-q2-1.7.4/docs/_static/scheduled.png000066400000000000000000001430371471170400300203360ustar00rootroot00000000000000‰PNG  IHDRā7Ĩáŧ”sBITÛáOātEXtSoftwareShutterc‚Đ IDATxÚėŨ@÷ũ?đWČ%‰$€‰ōCƒü’T@‘´‚­Vą•ŽēÕŽĩŗnmŋŸÖO[ˇļëŲ~´]W×Ų­ļ+ŽâŠĢ¸â*؁€JĄ%•āŒ%A ˜@îr÷ũ#?ŋ­ļ`_?”$÷ž{ßŨû.Ī{į ‹a@!„BM ^¸ B!„€ŽB!„€ŽB!„t„B!„t„B!„0 #„B!„0 #„B!„!„B!„!„BĄÛ›!„BčGĨĩĩĩ­­­ĩĩÕd2Ųl6Ü ß3ĄP2ę,†anúRIíĪn¯ĩ/陷‹âLŪ ØSųģį âĩožļØ˙fĪžúwĪ儚¯ž‘<úOīŌŗ^|õ)į¨ÃMtWë‰BÍfŗUTT477ãϘÂÂÂ233š\î°į‡ö [ôÕÅEeĩį f € xūBŠ$"\ŽX‘6ÂHįéeZücsr⮙–'úęLŽK•†’up"Ģ“§MĀŦ-+9ŨIp‚ĶV,Æ}ŠB]Éd*--íëëÃM1A477ŦXąB(ŽĐ- ŋŧSiöx‘ĸŦ&ŖÆdÔ‚3Ō¤?|&#ەeåj xÖdEœ˙UķØÄ_I…4Ֆ”+­@cW$O›YØŦ)+-7H"‹Ĩū¸ŸBĄ1ØlļââbĐ2÷Kiiéũ÷ßīŲ>ĐÛK \qV KM öį[LíŊæœÎ4 ×ö6[„BĄīĸĸĸĶų÷īŋøüõ¯ŊĘ4}}}ÕÕՙ™™#ēĨ]ã Žâŧį7+<û—-í䐞I˛ŗžŦ¨¤J­3Yxb™\‘›ģ8â˜ũéââ2•ÆQŒHĸäŠÜÜ4)뉀ˆ#bsÖ¯Kļūfk•ģ?ÜĒÜū„ŌYÛ×_SŒė ŋžÕi¨:TZŠÖÍ<ĄT–‘ûpNč<­í§Kö)5&+‚đ´ŧ‡ķ’ƒ=:’-ÚĘĸCUõÎ5„@›ēbõ šį$dgũ‚JĩŅ „ /žąNjŠ˙to™Æ o7Y)Į9b¸É—:F&“…†† čÂõ>nŦ.;-ˑKŨ#HøĶ‚‡Ä’ˇīTzöB[eÁöúÚĩ[žZŪēĢÖ:t#4ĻvNÆęŅFːí•îUg<šy]Üõ^!;+wmڔŦ&]mŅŽÚĒÔ'Ÿxėųˇ=ô¨>xšÜčÚØfCmÁ6Ģ˙O9.#ÆŗMÔĻo<Õj ˆŒ‹’+÷š0aûíŲ2y”x cĸėŖQzDF,ĄVS`(ßŊĩ€ˆĨáQōäÔÔäÁžņžúü]Ît.ÎØđpv8ßŦ.Ū]PkĢĻāƒĒؗŖvˆvVîvĻ"<ëąu §Ŋļpw‘Ú æÚwËe›“ũl/~×ŠÄ Ų9i2i6h”§-œāėMĪ$h wˆØĩ›˛ƒ9Ā5|Œsu,õģwšŌO–ą"#AĖąĩõ§u#ē)ŖI ËPȅíÕÅJæĶĨįōĸâø@ę‹ßuÆI^lî†ŧ¤`ŌPĩ÷ƒRÆĒŨ{Ūx,Ž–†Ŋ{Ũk—”ģ"YhQ—VéŽr‘q]‰ôÖÔÁqҜôØ3bméŽ5 É}2/œa08‚„åkeBŠ4XČãÖvuņî‚Z3€ĄøĀ9Å/ã8@ęĢęŸ‡ČrŸZ—Ė#­Ļv­FU¯!øŖ]hTîÚQāXIÖ3›ˆĩ ˜ģîų'õeųųĩfaÖĻõrpüĨč,ÛåjJŦøļø“ĘÁ øFĻ/Mšæ ŨÍuee &;Û?Z‘Ŋ­mö!f‹R—Æûø-Z›ĮũôŗæPEvjX0Đũ­˛´Rc qŦ=ei=[QĻ48¯ZØlaÂęûLëŦ?x¨ĻÃvķöQbbⰀ|ų†ÍšģwŠÍî.8ŖNeÔŠĒŠŠ’Ö?ķXÚ4čQ—8BŗËK–rĻ-^—§ĒßĨĻ eUíŠÕŖÜ§¯ŗļĖ%yO€i9ëķN?—o 4%õ=ɋųíeŽ~y^Ō“[s&¨¸ä´˜åoņ'Ā@G ŠŠ¸Z‚ŊÎÕIŪ‹Ž‘0qōdÅ(st•‰ÍËE& Ú fˆã“ú*WÍeoȉãĀ´Õ+j_.5XkK5ëâä}•+KōžzL1 äąūĻgwŠŠąÖDŋeuGæâ“J„ŋ$*Ęã֙ūqriuIŲŪ"ŊÉlĩ$éęĄ×ŠÛÉ8ÁTO{{§É?8XįÜŗCô¨>Ūæč™&Âs7oÎ{'ķĨq jm&"<*ʕBÛĢĘ ŽĻ´éŠÕQˆ į´?—¯JUĒąÄÉGIč×ĶY›7? å$ņôŽų‚Ic"Ķü9סM@œˇi]ō4Xą"ŧ|—įŦ:U•F÷Šlr\ĒÄ[^ŪZjēŪã :Bũ¨™LW˙ [éŨ§ęęøBi|d\HMšÁÆ™¯ˆƒūT™dé zØBšb”gi&8RE˛@Ĩ4•;ß ’ؤœÕĢ“‡5Ę䨊 õŠĢĨķĢoŒNmķĪādwö ÁŅ~MZ3)įl§×ŅxąŽšķ…Âqé$9îmâõŖÂ;ƒÃ:+Ō¤uíÕ`šk,9'86œ(5Q×y|ųã=(B‰+‰ãC¯ĻcĀúŪø˜°øžÁĀ…zCī™š†&t c¤rā %~0đmMĻÍÖʏžĩhhdŗĩ/üøėL™,L"•Æ,’ЏŸj õ˙ēM‡ŊŠšÎnŗŗ…ŠĐŨXwļŠ/@”?͛Į Jų@đãw;f:-(äjËe5„aBˆYiŲŗŠBĀ3†ÕZWsö‚ 2YļČŗļŨŨŊ6€“ą­Ī5GĘL‘‘a1"1ĀÂe[[;`VLörŠąŖĩšąąÃlB#č­omtũM2bø1gZT˛"*YdgũĮÛw9FčëÛÉ4˙kĪ"G –Wí ˇjũžĶę ]7!īF’3@’Āģū]Bē6&iíĄžķ~žŽ:Œ? kē>SIŨôTž<˜dÃģ˙ŗsH<åG=üâ3á‡JĢÔŽole6¨ĘwkĖŧ7u´‰Yš÷@Ɩ"nėJēåg2>ķŨļ‰Gõø¤d|M‹œ0ĮBĄIB(^e:O/ņđ–¯X+w<#•K} F° Åâį°§ÎĨ.KõÖŠT^hn䞟*¤‘BN3¸‹°šB‰Đnęp”´ŲíŽ:—Ķ{æŗBĨ ¸<ŽŨjåĘVŽļ,;Û13öˆĘą¨Ö˛O5ŲØ<ÛfĩągĘ<˛ÕUÖķf.ÍËŗé›šÄņ\›Ą˛°Ø/‹ ‰Z (ūÄŅËŊ­ŨŧиÔČÆCŖˇÔŗ|ë0Æ"8Öáa4ĩ;¯…I9ąÁ| ÛÕí#ŖÔâuQ‹HK§ūtáģ*+€U­l'ã<;ĘŠš2MQ­ ĀXž}įų-Ģ¯ÖNŒZ=Ú[{Ŋž”;ÆãôhŨũãÂÁ¨íôÛðy{›ŒŖeEĀņåÚöz=™Å˛ŊŪ@]˙ņ…BčG-44tė€îJ@į™ U‡Øĸ„Ėxą<Ō÷`[Ģb""›ĪB´Ė Āfjî†ŌxYˆÍ)Ÿ6t؉ŨŌ͞ĨXÆ>ÛdqŒ”KGo_wĢbdķį›š@–/îŽŪlD%čn}ĢE%›ŨĒņ[ ˆį5QX7Ú˛ŦˆÅ˛Č™~"ų4€yØLM& Mœ/ŖēÃ2ĶÂzO}ú¯ĻfHEqō™Ŋ­byČđžiģü¤‘‘!"oč=[S͐.°ƒWāü•wĮM•Į›BŌ—.ōšŽëJ_]V°âžķCš+Ûl7q’pzęäĢ ķyb™,B,đ˛]§Rēž?(uÜĒĪ?VKėVSĻŌ? Ėˉ• 8¤šŨ UŸVÖęĨ^Z7Zž NRHŠ `(|w/'O! æÕlŌkęOWך’7ŋ”Ė‘f¤ ĢJM`­Ũš rs’%0Ī՟6+6?ÅžĀ9ÚĒ.Ģn ƒ9ĀFHGŊmŪøVG–K8` ˇîhĪU$ÁjŌĒk ŌuO)ÆųĢ÷Š"I¨,7PęwX—›!rœßƒŦ­Žˇäŧ¸%͟#͈%T*ĮĸŪũV$ ,ę˛Â‘Ē‚ĄsņĀîŊdĒĐT[Reü~ë0ÚøB×f֖WÕs$<āøK#„îAĻڒúäåRR]ŧģÜ4üjŠd×ĮZqjZŦT8M āQ$E‘Ŗ‡[āđb~FHnŨ­˛Jwėđ߲Y1æO„rŽOÚĢĘN b_!loVåŽ]‚ŧ,ߤ*.tî}"A!ũęī&ĩ‡ņl“ņ™–°X\XhkíŽwyŠ(ĐV˜×xŽ/|oBĄĩ1_ķ‹ÕĒĒ×49z˜Z!2Z!‰ņk¨9rĘO‘˜šBÚ­īč>ØM eĮĊԴË{[ TˇÔžŗGJiŅŗâĖčüļúxM›Ũ5GĒųŠųņ‹Ba ģųXåY3ĖQ {›˛ŦžŸ—–-‹ņLÅ1ƒÕÚ6rYļļúēVŅ‚¨L…Xßd¤†,_SQ!R¤ÆdfÕŨ\]ŲĐcˇ÷••‰–ĻËŗ—ËŒm ˛Ė^}sg\|؂„îĸŠz}Xšüž‡ŖģŊL ô•Õœ-ŠSŦŠčļt0FųË3RĨŪfsW_Ī•ž`jđ,yęŨšwE‹ølēëĢ/O´RĀ M]š8 |ir†<zēēÍŨŽo!x‚ éœÄ%+VĻFLõaÛ7"-C.${Ėf÷DņŦyKRæJ}ŲĀÅĘ8íß^0^qJīđ Eœ˙ˆH׹:"yFj¸ˇĩģËė\ā %ąIKDOã \¨úōŒĀ7ūŽ%a>dûņŌÚn~xƒ86ÛVú’XŲŨeî3Ž^DüÅĘÉaŽq6>Ōy ¤ũŪdĨ’¤•ŊPá¸=?z‰b–/G4W.ėē 3tBYúēMyĶĪVНäw-–úíĘŌZĶ-ĢÃ(ØūŅŅ‚í…ŗc+x§æ$‹ĻEĪœ!OĢxdcfßņ# œĐôĨ‰SŲl‡mĩũVˀsO!G/XųČÆ{\›tčęp|#’ŖÉoN4™hĶ™S„Iķ¤ü‘ģø3ãƒût.v[•‚ŠÉ9éÁļo؂ŒX!Ųm2u™]ģ5\žņāÆ9W×~õö0ĘfÚôU‰˛•öhíėŠ×Ū&㜰ũŖÄ L­ZģxÂØŒ‡Šp͋MĪIžÆ†q_!„~Ôf˘ņíˇß⏉~ĪęęęęęęŽ>¯¯ovvļįzÃ0¸íš4Hũ§/ouŒræžú^A!4n&“Š´´´¯¯7ÅÄÁårWŦXáų Qeˆ BhéŠ~wWŊ$#C.  ¤Ģ-Ųëƒ.Q$a:G!t„Báũ÷ßôčŅŅĮē īŨĖ™3—,YÂår‡=Ą ėŅŠÔ:Ué°§yąk7eLÃ̓BčúpšÜėėėÖÖÖļļļÖÖV“É„ƒ^žĄĄĄaaažãÎ=á„&t>חí-ĒÖhÛMîoeHerEΊd)ūôB!t{€ŽB!„Đ⅛!„B! č!„B! č!„BMlÎģ¸\ķę!„BĄ[-11‘đ|€[!„BhÂŌjĩē+ž¸nc6=ā}ĐB!„&įũ÷X X š €aXĀ`˙ }‹LŽ"ĐB!„& ڑĪƝÕX,püɸūf`šĀÂ"“Ĩt„BĄI†Ąi`ągÄcXāˆ}ÎĀĮ0Î?hÆ1 €Á"“Ļt„BĄI†fqÎŲŲĘxų¤&L đ˜ĀŪsšŧÉœĀ2_0ž¸ĖŽ›6ƒCS_Ō’ėé!SX”ĄŨbĻ\]ņŒ3-: ŽĐčZŠc|ƒEny‘[Đņž0hÂÂ/C#„šÜ Ž”,/į sˇĩ‡ŲGQ }ĨûŠžb™­všņröŅ2 lQД`/ŌÜvĨ‹ Ú1Ú5Ú= šîĨx0,ho`a‘*r :Æ 41 ģtÔjĩ?x•"""pŋ „ēŽ€ngĀņ•B°†ļ;žĻ.ēžíwMİ|ü§H§˛zĖfŨÆ 9sã„Á^‰K[Ũח:xüš3Á|6 ßbŅ\čnécˆĀ œHŽŨbšÄō æŅÚÆvõÖā` pĨLwŦdXĀĐ0¤b€En¨kh@÷ĐÎ灿â.ŽĐD6ŦĄ†„„ü€•9tčPxx8î„B×ĐÆy#÷č NtÜôhčŅĩU\ĸ]īvŒv}Q‘&/´ô "|ũÁnЛM”ŨÄxËgO !l-ēîđ‰ Ÿ2w6\Q]žĖ0ĀæķxĻŪÆ.Ļ× øc>Ë9´ÚÉ1†ÚãUWŋÜPöЀî™`ŗ¤(ęË/ŋ>ūžûîķķķcąX7–ãšČy!„ščv÷Ä::bA¯ûō•;0ŊVŠĻY´32´+ĢĶ4ué˛åJ˜¯ŋ—ũŌĨŪf‹ Ā.œá˜ˆđöfLŽWēNžīŗ8Nöô:ĶĨãæ€ŽŒ9¤—Ø=Ö‹ÜPЇõ “$šgĪž3gÎĐ4-‘H†ikkS*•.\xúé§ íkĮ ƒ&c"§iz<…<¤ņ.gœEđĀA!t8îÂ8ƑPú —5ũÎĄĀx9c9MÛiW_-4ímgh†ą;ØúNīĩØY °˜>Úΰۨ~čÁ[2ĀbÆ5@ƒaßo°ÚYΊa‘+BŒĐéüË/ŋTĢÕBĄp͚53fĖ€ŽŽŽÂÂBFSXX¸~ũz‡ƒ9ũ8: ũE¸‰EđĀA!t]ė4, \o`Žr†ĄívGøsß=€f0 3vŠļQ\B2Ũ6´v÷]ĸøĶšSfښú>OęOŸ9cpĄ]Cܝwįīé8—B3îŋõņŦšą"0F@gąX$I:žNw˙ũ÷Ī™3Įņ|XXØēuëŪ~ûmĨR™““ÂfŗŨč˜3Đd 讇=§w=ũĖĘ4=ũū3’ī|úí?¯•Ũœ%öŸūåü_'}ųŸGÄĐB}÷€Î0,šĮ´ƕņfhÚyĮm†ååH‚ ÃØGūcšĻíũį.ZƒÃyĸĐ@Ņ´ŪōšÎ“g:“"$!ķėŲeęéĨhģk–vgwŊģŖž†åž¯ˇcŧ‹W5XC+†EnŦȝa’$ģģģCCCĨRŠãyĢÕjĩZY,ÖėŲŗkjj.]ē$‹Ŋŧŧ0g IĐ=č=˙Ūtߟũß;röž>Đßrú‹ V Mûܔ%Ō4ÃCŅWBĄë}[püÜ 0@ö–Vö9FH°Ü_Ĩé‹ęæŊθGÕ(ĩ5ā…,SKëp%B`™ÍĮŋîpũØĨŖHGëŪįģ”s)Ėāõ€ĮR<Š0Ž?h,ōŨŠ\% ģŗ„Ífëëë3›Í4MŗŲl‚ ĻL™âˆ5vģÍfcÎ@“: ;vˇ´ ˆO”z3 ŪŌ¤Ü˙—Ā0LˁžxŗŦĨĀ?ūņ?}đxŧOËž5÷ūĻļĮf(žûāO?—ų˙ļ4ŗ(énīōå-é…ÚŋĮ•ũæąßīéīøÜ?ũũˇ>Ė€ņø;ū­č‹†˙Ŧ× ˙ūxŧ8!„nŒĻYŽw÷ũú\_|SqDB–sė3`‘ÉRäjaŸÎÎΖ–Įfŗš\.›ÍfŗŲ3xĮĖhrtéǧîūã/RėS$ÅÅ%-\¸,+Iėũgļox_úúҝ“| įÄŲ›˙ļŦôqŅ’ßW4Îđ÷hų8;į…˛ÜÂģčiéIüāāÖBą´ėĘū߆ÜOŋúWŧOˉ= 30Đ#Øk• IDAT}øĶ¯>đ?˙ĮĖ•¯•ũ¤đnĐß3‚v =­û›w^Ø\}^ß30 īu[ihoQÚC+įqišî8^t>öŲbš4Ms%ŠK$Đ_ËxKsVĨqiˆœį} Ŋ‹ĻũđĀA!tƒŨÕCĘrũöëŪ|,ÆuĶmpũ‚Ĩ+ôa‘ÉUdô€îå啙™ŲŪŪŪØØ¨×ëįΝËbą ƒc‚îîî)SĻ`@GˇI@Ėŧ˜ĖŧĮā7uĪ-zđã3¯ū/‚%oūû/iƒRúŋyî'Ī柨Wđ~œõÆE[švü„k9Í0´įˆsšq~3‡ šŪ>ā1† „B×ÉN{ô¨:ŋ}čŠv,Ö`Į,Ã8îÂíüö!™$EÆ č@„¯¯ī}÷Ũ÷ų៎´´TVVÚívšĻg˘aĩZu:Ũoŧņ͟ū4--Íf;ž*Š9MƀÎ0 @˙ų=ŋ-<ôXNœČ §áDCĪŦ$Ūŋ%Ū;ßĘ?Sđ‹8č×Wíˆ]bîčå,‰0ŒĩåŧžœŊŒ{ÄWĐ9Ō­;jRší Õ=Ōy ÀsBĪ1 #„ēŠŲ\ܡąŽ.ː€îîÔķōōōōōōööČËËëęęęéé.—ØŅŅņŅGĀž={hš^¸pĄ#Ŗû~Ōũ†5T†aŧƒ"guėx|Ņ˙ŧŊŊŊĨŠíúčg$ŋ.Øöōķ?ŸŋcŧA0kåkå¤ūæ7’MĢĶ÷ˆD‚ īgÖvGt€Y>zUŋiíüßxĪZųÎGwxžĖ ™t”ú 욏u?„E°Á"Xd’ ÄwķÛ: wëģ¤PWW7{ölĮ A8ē)Š"I’$IÚ}'u›ÍVWW÷ųįŸĶ4MĶôĪ~öŗ… AQ”{ÖįΟOLLÄMŒ&ĪvŪŪŪ?`}ĒĢĢŗŗŗqŋ „§ÎÎN 衡ĻĻĻÄÄÄQ†¸¸ģôŋCD¸ž9Ę0 AķæÍŗÛíÅÅŰ˙ūääd///ü¤M 7úKĸßS}B!„Fââžģ9‹År wņ ,‹ĻiGyiié† ėvûD:ĮhC\&P}B!„ŽÖƒ>*‹Åår†INNž7ožˇˇˇc:vĸIaXC=uęԄĒB!„Đ($Ékķöööŧ;˛H]]n\4ÁēŋŋVVt„BMЀ~?jŸ1cnY41ᐄBũ:B!„Bčģtü¨!„B! č!„Bč&![ ˇžW×?˜ôĻJ2WįĻ„rÆYūÜ_^üH S37o^&âyîÃW>ŌPž ūߖÕ㝉[Įám;*ē@öČÖGŖ8×ĒĒĪԈÄkîÅûŧã„BĄÛ0á…ÄÄNĨ:´š6}]Ņž ˆ-KD×7ƒŽãŚôGcũž‡ēN ‘úô_jëęŌžØSúâI~¸ûÜ `kF!„ē „d歕ķzëßŲē¯ z[zI NŊŗŊ¸ ¤žø„Ü ÷ä;[‹ŽDiŠļĘî ú,Ųz˛čĀ—*}„¯4qÅÚō@č8úŪŽ’6YąųŌ§ļØöŪŠ>"bõÃAGvŸęĐ|ôâķS37oY6Ęe‚4gãr^ĮŅm;Jēā’ļ‹LōƒŽÃ;ģŪ­įūōĘGZ˜šķü–%~-[ßSõûČÆZUuú>đ‘f>ôč˛HŪíĐ#""°5#„BMXZ­vœSļU¨¨møÄĻ„r€—.-Ū¯×?×+Oōë=WÛéQŖĻs߈ĐjOĢ—<ä}{k?z¯HKAPĖ‚Оj<ĩīũ~[ÖFϝ]ĄÚQÜV\pÔgÆŠS}@ČÖŦ]Ür9F[Ņx Ā'"!v*q•Ũ{ŠÍ 1•pÍ;~÷kNhe‰ !*U›žbEâæe"Îí´ŖēŲlÆvB!t ÚUmΰ=#b*~ąKdE{4úãęËI -uzB–5zhæ„dŪI´ė×ŲēzđŲËĩZ (gíꔩĐ5ĩí%]Ē˙hs#cEéåÖo/Ō—ėođ‰Y›'÷ˆ]ļŦEÕXŅ32×äE õû^y~˛`ízĪņ-ÄØë']ķĢG“ü:üZvTtu5]ĸāö č^؂B!„n3Ō_Ųž}û‹,đ…>MŅGĮ;€'ˌõh;ŪØĸ­k "ŌĮėÔæø%ŦHŸ }§Škģ(įsdßĨ^€K%ÜúĘ+[˙XŌĐÕŌ ˜¸,ÁĮqAx—ėzF‘!!>@ĩi›ēFë<§F<ã+ųáįČđäíļû/MØl6ļf„BĄÛ†ßŒˆŠpĒzĩ]ä‡3#3ÁWuęŌŅĪ÷Q@ĤĪā]%"Š2WÄßĶØxÉÚ}ƒüú!háƒ+{Ū‰ Š@ļŪ¯rܑĨīDᩔ_Ĩ‹8ā3FĀö’ųĐF9U˙áļ}šK' ū“˛y™ˆã(HY­ôļtŦq[ī¸ÁĩķōÂŪt„BĄÛA[EaA#ôļ4ę‚ĸ‚8œĐôôЧJēô—|bĶ#ŽūÕJ^ė˛;ƒKÜŌ#JŠ´—Nũį$+"z;ÚZ´]˛›#ɖâ‚]@Č|$ĸâ¯%mÅĮeŋZ"âđĻútļxO*H–š,iˁ(~ōé%;*ēēŽÖfŽšB@Ĩ/ŪSØįT]?ļŨ‡Ą!„BčvCĩ5ĒTÚ.đ ’-|čĄt×ÍÅE ™!āĩ`Æ5‡m‹RV8Į­8zĘC˙/7Qę×ÕxĒĸâ„JÛåĩ v*ÕT”Ē ؚ—4ĒēS׈Ųĸô2€j,Žģ œ¨k‚čĶĒZˆ„…!?ļŨĮrü>Q]]]\\ܤ¨ņįŸŽGúŪÜ{īŊîŋ/^ŧø]f5}útܞ!„ž‹‹/~Įōš|ü÷ۋ/ų$ūęÅŧܤMSSSbbâ¤Ā“˜˜ˆû}ęęę†=rƒņ‡€ŽBč‡ÕÛt˛VĨǏ05%3ĶųÄEā&@hüŸ8!„B“ŲUw¸¤Ž|#rēS„ų|Rtŧ‹ B# ;.hÚĀēŪTī(‚‡BĄgFŪĢÛķp;LĒ€~-TGÕ?}ë›ļf՜ąîmIuŸoŧ3;€č>ũŲëĸÕ"ėĨGˇ šfn }E:žŠ¨P_ļہ7c"#bˇ0œ8|ŦÅÆö W,K:´Ū˙–ŽmˇÚ€•–™"á_wuŠŽĒũĮ–Ũ77`œz/¨/ōfÍygæm˙ŲJ˛rÕ>ļ'„BčęÅējŋā€ņ›‹ė@˙>ĩŽ':aŒwmÚ|ū›˙˛įČϞüâ–Üi÷į°X¸•Ņä5ė¸âŌsúŖW^Ũõī3Ūūŗ“īyd˖ĩņūãŸÕđÔZSĻf/ŧoíLļIUüEÅE÷F_+ĸSjÜqßŅC’.fÆĒŲ|oĀŌôågÕaŪBÜĐZŗXã=zŠ+ēoÔA3ŖÅ>ãš9\×ĖB! čcäķvu;7បŽ’˙Ŋ—ęčŦŗ4+žÔvÛØŧ™‹sfjNˇÛzzŠ˙ų5W”’ŗØO}ô?ÖÅ÷/mĒŖ•Ē;[8{á’T)zi$|ĢuĀŌkĨä,ãE á]îčÖ[ģvíŠS§Ü,XPPPpÍTMĶ4@˙éßÜšæ¸âŊũõ÷Ėđčo9žī}5]?‰õģÁ€Nu7€ä.‰‹Ķdqž_7´[cžÁÛŌŦüōäų;[”ĩ$!°¯æČ‰‹ļžË_Iŋ'ŨũIgĘį°BÚŗ=lŅ+ŋ<Ąí6ÛÎ&ßs—ŒŊōčImˇ €í;3åŽô0žGÁUUeC‡ x!ÉY˛(ĶŲĒƝÚŦĀÎËšSØpō‚ĩĮPúĪ˙˛îēKÆ1CKseɉ V6ĪW$ûđZ!„¯ĻĻ&·‘‘‘¸M~´ŨŌōMG@ÜâiĄĸ0û— —įgŠ čmüO•A”ŗfšØ`ĀByķ…ķC.¨ĸWތôčvö“ŲÛN”7pĶ×Ŧãč(¯¯YÉģÍʎ^ž<ԛ2VüŖü›ËŌT!Ņ­ūၠŠëV†yãžAˇØSO=ĩnŨ:·ãIՎtã× ŧŸ¯|ũnŠã oé‡w,`Ļ˙Ėߞxâ':ĀvîëŧĄëŖûzlÜžã'o??ļírŋ5Å}|ZšŽV]ßõ“UĄŪ–Ļ#ûŋ"ØŽOšYĀr~âÍãĸŅ÷ÆŨ‰>V÷9Œ>ąļö3ũ MĶ4ô|ą>mcš`֋åG—•H{+3ˆĻinėĒõŌ7œą<äsÍC ŋŽí˛ÕÎd_ŸŨ;Œīņũ!cUXôąæË„ň—ī™<Í} s‚Ķx0ĻU¯on:~ &tYŪ|ļâŨÄxŽÎĄŒ,÷ō|eŠûŌgTįy#ÚcÕÜĮķČR­Ā&gÂuėc:BŨ*æļß˙í[Ŗ,ūe7ãnŠ´Ą˛öoķŌbßŅ^Ĩ ß´™Â¤ Üî7×ĩ'Ą:m3—?ō¨ĶƒéĻúf‹ˇX&ė­¯7Ā€eĀq9ģÍ6,yDŠAß`°Ā€ūL“M)ĢߞęÖ7ˇZn`5.}v_tddddddÆŗ_ü`ŅäâčD÷š# Ķ47r‰´ãDŲšĻiŋœŋšpá܁DŨ´…fډaœœ#ׯvĨ,”IA_o°Pš†^at(čQ$´66ĀŌ\¯qXĀGÕkė´PŽŋšĪęí~">1äån ; tV|ÚĸDĄ­Ãdķ˄}õuz‹Ģ´ŅâY­(‘MS×Ü똥ĨĶØK‘ĄĐ\×äzÎB›Ëļ÷PŖÍˆ °hôŊmš…­!„n!Ķš‹F??ĸŲpūû'”îŒū”™ÆÍ~]{ˆ ejh˛Iƒ7Kô ‹ P64õFÅ/Ut—•íûØÎf?LąrQ¨H eŸũŊŽ'Z´*ÛuĢ"4]!+Ģ(ü¸€ S,å `uY}ÍĮƚĶ~ēęēĮ Ũ÷ŲŲû<˙PŅds•žķQ9Fĩeŋ÷Öڟ?+ųč…Uŗũ ĮØĶA #IMeōķĢķ–ˆúŠōõąÅzīˇˆĐÔĨņGĘ ?Žļ_XúԘĄ_7åĪRdv9˛īc;W$_šJ Xâfī>[YŅÔm6O$[ē4jČ=^Ŧ­§ž¨é°°ĸ3#ųāŗtioÅąÂílļ¸"yöR1×5šw˜bYbEÅÁO”Āļ[ŠX&ö M_6ŋ˛ėā'5Āļ0qÅŨ1~ÂčHūáCûš¸ÂÄw2ÀYŠTÃáâO5~|.—ëĮÅ/‚#„Đ- Ė}ĮĪPŅYsfŸøæËf[L×ŅūŪ%ŋŲŲ{Åf"Ļū|Ud8tūuOËėæ-}ūpÍ?q/-ôč#¸ü¯ƒšã—'đ™BÃTKķų?Wu™)š´á æü|ޝĩQûe§Ũ|äĢ˙ķ&fgÍŊ?ČrōKÍ Ā–ū|Ųt žķ¯ˑęęę'E÷īß?YNJ&ģēēē5kÖ¸jĩZį|ĐQũÁ;ö”¨/x{Iį-ųŲæÍÄų÷7ėyúéÕŪŗVžļëĩ‘cōãĮ{Î !„ēZ­ļĢĢË홑wq!ÛÎ˙îũķŸG 5õŋûZüŌēІʚmÄ[ֆKĒņ`íŋ"äŋžËՕ×ūķŌb_ÎĀĨ÷öčį>0oŅā0ęüᚏØs^ZČŋréŖ=˙mŽšãĩÅž0`ŗxsųpĨķŖO 1ČSĻôÛûÕŲÅ)ŋxĐēō¯ūá-û߅PįËŋú;fËb_üŅŌņkjjJLLċ„ŽÃā`•i)žŊ˙Ҏ‡ŋʍųé{Ĩ?ez„Bč{y§jŽ7,.œˆ”„U6יC–Ā+P"@ÖN >áōŪgúæ…ҁzCôįžƒČ)‹ĒH\Ā€)Â%aœŋ9Ō˙•ŽĪ‹õį-@mîĨ8f:eʐRuMÖËlÍ¤h2¨Ÿ č× :B7ĐBĄ hāōŅ&ō2Ô?{€´3Æ3}KōØŽorŽ@ xéԋG4—Å_Ûb– ¯öĪlĮũ'7âå[æō9`9˛įÃČ;øąŲŅKį="ņÂ]ĄīÉø”#„B? ssëyAÄĢMŲvöˇ‡.ĖcrîÜ˙üīyžxKđĐHMđBčh,d/‡ęĢ3 ´Õîčà /OuŅĶŧøXh/ ø‰¯?Ÿhkŋoz00Чģâˆi:Bˇ’į"„BLc­90^&t=æMO€†#í‘KĮ(Ā žžāŨŠKš<<ŗŗds‹ˇ}ę#`{ų9¤šŸ˛pę{GžúŊ‡G°Å|GĻįΖOũâHͯŊ}–Ę’wߗš?~p6?aqLx î—ë†_EhLÞ$ŠBũ°Æķ%ŅësY˙ŸõÜũP\ūNäÄ0‰ŋ$ZW‡ŋCˆB!ô]Đēō¯ūzŽ_8ĶųD3ų:öh"„B}g^áYI˙—…ÛaBîÜ!„Ba@G!„Bīâ‚B!4i|×o…ĸÉ{ĐB!„š@{ĐĩZ-n„B!„&J@ˆˆøîŗĶjĩ7e>ˇn†ß§I]ų!Ü_?ž-ĻÕjCCCqŪfZ[[ņF?†Ķ×Õ^ļ]Ŧ*QvØhš///ļ01gI8ījÅė]_> —Ū1•cĸt„B!4‰q§gÜ{?Øģj˙]AĻܛ*Âč!„BM<֋_Ÿøēš—đÎIM‰rl_WlîΔ@‘m÷°ĩÕVÕˇ÷ÛišæÆĻ¤FNSÍŋ•œEËî˜ĘvtĩëŸ˙, Ûlļ¯žúęōåËvģ}ØKl6{ęÔŠ‰‰‰\.ˇ B!„Đ„Žį-'k. Ūs§ˆkm9^rĸNxOǰŖî¤^|Ī!\kËņ’Vđā c3–%qĀzņxY]s蝑Â9ŗŲ˙5ÍMąíĻ˙ę!l‘ĶųĐëęęX,VJJʨ¯j4šēēēÔÔT܂!„B˜ŊˇĨÛ'2QÄŪôčāzåEŗsŅäēĐũœŌ1iû7'Î_ęˇ@ŋĨŸkąGōasüžT_´Š‚.Š/ųDĪŁę?d@īėėLKKŖišÅb1 3ėßYŗfčũũũũũũ6›mŦ­VëxĀ—,_Ÿ›$$åŽ_.qˇt^lVvvV’”4¨Ëwŋŧ­¤ŧÎēK?(ģîB/<5Ë![ŽoĪWgŲŽí…ĩ:2"5;77+YÆ3Šëu– \aNpFvVVVF’Øqų—”‘•••ü}g ^lVnnvĒ„0ë”ģŠ'ÕqÚQ–_iŧ…&fĻ)V(ŌfņŦĪŋ_ØLâqvM]]]¸žđŋK÷›ĐßÔd˛€õâŲvNpĶ…tģŪl{wKG?Ømvöŋ)l{×Åæ^× gŽhŽÔö͉oúƒįãčæīÅ`:Ã0C:H’$ɑ}įî)ŠVdÔų€06u‘ĨŦ¨Ö ‚„EŠą|†ą8&$(r3„Ë“vŊđžĘPüiÃĸ§äükĪĐãĄąčƒĘ¤g¤Ž§a˜îĶģŪ,P›Ŧ‚đä5Ö& 9–úíĪŧ¯aL Ąk4Z qRîr‰Ž´ŧÖ`&$‹6=š.ÖHĶéĸüƒUĀĮd¯Û#ģ ÉyŒĘ R—įe]Oښ>~îMĨU˛vû‹ūϞ—_ØodŊđzŋęåö IL„UŖ1Q„8iũS’…mYÁūrĩÎdāI’Önڐ,Äļ|3 Û_ũiđdŠYŲŠR>Ŧí1Yø ÌØëƒ빒?W*UFŠ'Y´áÉuąū=ęĸüÂĒFŖ€Ę2Ö<š'÷°hJvī-m4Z ĐŨ€ÛŧđJŠI˜ũĘ+šÁäéíĪ|¨#žųÃ&0ŽÚ9]PZk0€ <ÕŅÆŨ´$#7€Ôî­¯5š˛Ŧ5ySÕī6ˆ˜Įß|JÎ'5ģŸ{ģÖž~û3‚§ßVQ<™L ×­ ˆYķä&…”s‡ÃX- ˇæĀĒÜū„Â}ûŠĒ ˙Đi‰ ’äĩOŽ—ûŖæžŊ8˙ÄOL‡Á­N^ūęPÁĄß^Ļxĸ9ŠŧõwŠžÍ˙ŋŋļōâũõŖŗ: ŪøSM//ūĄĮg•ŧ˙-P˙}˙˙ˆrû›ÅÃļ /laŽb:įŪˇ¯ę°v˜,Lčå‚ßžScžæĩg .WžņZQ‡ßâg›+ę(|í­ę^ŪĖxŅå3z7=íĄyŅ‚ŲQšgΏ÷Ū{nô”;ô•QR†”Cļ+‡ĩœåώwj€Rī|b#ˆ×ŧūĒÂ}Ļuž­HRSųjĨFøč힃ÛG;xÍecœÆŅ¤9áwWo}>ßĀKzîÍ  ŲõôÛ**üņˇŸ—ķG{/&Ô%Eåjƒ™BžēūÉuąœŅÎf`Ē/Ė߯ԘŦO›ûÔĻ žļŦ  ´Ū`Ļx‚đÔ ĪäÉnZį oFĘ|͉˙.˛p„sÎąBSÚOœ<ō6›ããã8{‡Í >Vsä?SĻp؜)>îvöTYä”ķMĶgãđķī= Õv¯ūīMĀUČ•ŠŌŠÚIų¸?ĸæ%ŦI2ė?fØŋģj­ĮŊ=ųÁ1˛‰0Õ*#Uī—ÜUņæŲ­ęw4ŧđxãú%û•W–_uZĘ ’ŦåÂڃJCmÁĄ,ųzåÎ7÷ë€'ËZ“$4kjÕ: éŅĐÍ×&I@g°Ēō_&xb™, {mũڍ‹/žSy&˙Ãü°Ž¯{!pņƇæ…ö 'ūTÖĶ.Ÿå'öe¤š÷2a<ņuΙÎģVŦV[ŦŦ.ûúbõūšÅŋŊSüãJįķæÍkkkûüķĪĮ™ŅĮķÆ7˛!Ŋ(SîŪrōuŲUo—A’ũxŽL<ĘĨ‘átŊĐ_( žFwΈĶxžą'Ī ŋįĒgcĪ7ŗÁđņÎR#˛×Æ ÚÕJ]ģÕ2ęŲ,Û°ûũrH˛].“FĨŗX,ÚC;÷×Zy kO˜Û5j#IÜxCaOMē7wČŲmúwNŋcčD\ŅK– ? NŸŋtúhgĸŽv20:‡Ÿ˙ā=č7Ō=<֓ž]Ėā ãŠú=UĖ8zDGWá=,RŊuLw°pL‘Ô—W—ļ›,æËŽ˜`cŽŖ€ėž5ĢdæĀúÚ}FŪė{VeG˜Ąžö ŅjęžŌrŦŪ šƒ;ßtĩD}K3û;wsŒQy^xJĒchK`ŒÁ\ņė õÜŽžČU̞Ĩ¤XĨü‹ÎÚiaēÛĢtDĖC×Čų‹˛VŨĖË%ėPō80ëéßđūũīcõŨeĢÕ¨Qî[Ũūėsá#wŠņŌGWMĶõ/ūĶhiŋbëT6€Ąüũ7˝ķlWw^!”€dÍÆ‡2ü-õĩUfö?¤ĀĐFŅ}ŽJ&eūÛJ×ü4ļŦ@ÎčŨ†ŽÂŧ˜{xš:]ééhTS X´t6Ī5_aJJR?†Š>ö—ķFU[÷lÕø‡1Z¸ņā›/tl‘E7. dލËT€YU°SåzwĶĩۘpG#OILB ŽLU~Y§6hôŖN,Į„Ciuy•šĮj°tZ˜Eaá<8œĀ°˜˜˜Îq€Ō”‘ááYŌ=‰ŖŽņđ|ĖÃYúĶÔ3ī*/*"+DčO|Ķ ßúË;Ž rQßCGĖXų‹û ÛūŲôõˇ@DŪ˙˕3Ø4H#ũ Ŧƒđ”EEq†,†ĻzĢ˙ōZ5øÉōËĨAlš¤;žĻi×ôŽŋQęʕ‹/ŗĪ|ũ¯‹ņĘM˙XÂŨ'Ÿ|âHįâČčāúOš0äŨ…Ĩ!Ųbˆ-‡āøÍ@Š‘ĖŽ‰‰á ™ģķ}(áÉíË8Đ^tĩƒwøiĪØ“č„ŋõĪ64øí8y#h/zÁĀKyäŅU€EYõÛŖžÍ‚  UE‡ôRIxŌŌEÂÄãX­ĒCEæpIxĖĸ{OŌ?4{×7G5Yüæ, ÃáįˇY@š9‡åuËš* @„'ˆ‰qt×˙ŧđ•?KŦ}¯ÎŲCÆ0L[Éūđ/#–žfU,yrĪ?­Î…0îÅŗ ÎāÉ —üę‰EŽÂŪ„CcŒĘ RîšßŊ$†49úÕHr”|îü“ ܗ!Î!ĢžãoųųšlWˇûĨÜŋ1å~Ō¤Ú÷Ö_NšÍ:ƒE2r Ų]lžŗ•Ųœƒ7ūĪ=bįsžqŒ;&x„Įß=3Š´ žąŋhsœũŗg œ1˛1Ž˙]´aKSuG ˙ŪŗßJxɒp‚aHį2€aÆF:æM\×á0f đĸ|ßq#˜{HÆ]qŦ|ö‘W—1GČcĖžíąQ@3ęÄŨÕožņ÷f§ÜĪR^ãžŋŸ4Í] Ã0˜õĢg9GĘkĪë uēÆ:ĨöWۏå# ģļ˛÷ŒģŊģžŸrMį¨üÔEKuW!ühšÚu8RV˕:€āší`ÖļŸ¨{WŠę?=ÖÖ^ŌÉĢ@Úlƒ<ē#¸{ųƒ~ cAA;;¸3睝īužr=Ž/€åÎQŌ¨-G6ō„<ôŧŽžöÁëyĮ“÷ä9á_qDĮ;4xœ4GžŌÜÆ8õ 8ŸeŽ=ŠŅÎĢNžWÔ1˙÷ôʧXzLÕlĐ5ÖéëjÛũģĩŌ q9Ξ:÷Î{įb‹šŨēŠņdĒ GuLÉKL‰á9&0זuÂåķuuÍV fß˙“š˙ŸŊˇkēú˙˙Ÿąíĩۀ p Á8åB‡\Ģ3 õ#P %’foÍwhĨd ~û)š‰oSú¨ä^„úN,?!j,E@d„ "8t‚Ā6.6ÆöÚŋ?† 8” MôÜoŪjgį<Īķ\=^Ī×yL”a ĪÔ-ō})Ĩ†ņ‚+īĩ€)s$ÃTy­­Ķ¸÷ Ŋļķv4ĸm›ŠP(Ë9q†>ÅŽ‘Ü–*gÅÆ¸™ ōø‡.cĶ=‘B3čŧ)ķ‘u/˙ža˜?˛¯W.]tW_Üß8´īįYŪĖÎ*á5ŗˆĪ"†ŖĻĪ`žn+?ņmú~{îHQZUÕÃxÎöŽøcMđ‰_õŧ&2xžŒė™đ§3Œ`OHĒ„Ĩ2ŋĪ>åaˇęÄg~šHĨ)>˜đM‡™@{ҙ߆’Ü{Ŧ§BŨÕ×îÕŨúåÄĨ`ŋá¤ö;׊î ˙đŗˇYEĐģ*oC•ˆÃ§L–sęūŊvN°ß°GK HKōķÛڋN‰ˆŖ‚†Ķ˙Ėp觇›r\ƒ&ēĨ_Ž(ŨŸüķ°/ŪæšoUÜ;ķĶoĉ#Ií÷n]Ģ"F|ą€Ū ŧW”Ÿ_u';ˇ€áÉcsė$ū,XŌĻ ûaĻx•Tųđ:۔UųE%ĘáÃM/>~SŪōĶÜ8q į>ôŧî@¨ĩĢĢĢ‹â2gËUŨ­hãįB)šÖréÔYęDšļОäēęÍe95žÚ•^ŖĨŧîÍŽ/ēũ{Ķŋ˜ëdÚE¤´õW¯–uąŲN#ŨÜč.dķēg˜'ĩ}ûžkÕéģNŲ|îDĄ™¨šōŗ/mꋚúŠ;0zoí ¸¸xāKĪ;d{~æOCf¸yˇéHøí´ũFz‘NĐÖä—ĀđáŖF2IũÍåOŧ=§q$ЇЄO'Ęh Ŋu6ķb­öÚm÷OŦÅoŋãÉČΑ:4lŠ›ŠôF˜ķá‡AFgŗ[ûOH¸~AŗxPu&圔JMųĄũ—č~ŧ`Ήí‡ åšū&/Ä+.Đ „§l3"Yö”ĩgŸ€\xę°ÔÖŨu”!Aį­Üsˇˆ4{Wū˙DžåŲ_ĀēŸ × =L="#GŨ8pK ĐÕE´ īXąđđž;ŖÜL‰Z#ËYũLGÍ_Ŋvė˙ŽO€é0˙ą">‹ēŅEvؔpßŌWî_I?cīĘ4č|<0øč‡Œ)˙^ ĮŽe—ž;V`:l|8…ˆņ3™¯iŖ‚|G喖^É)"3>ü­ˆ)Ãē`˜‘&0ĒĨˆöáą˙&;UpãÜąR0eŒ ˛%t1‚ÎŊõŋéĨšĮöß5ōá- š{Ÿ“"—ž:ußu8 @Û{-īęębLųt%KĪ.ÍIŋDĮ/ȔØī“q=ûÃ÷MΊ#b zü;­įÍ,ų­ėŖWäZ"cԛ‘ y´.  |8<Ї3}~rgķwšuįļīū˙/ŽÕK˙ŋŌÜôÚDšŊĮ[4⃛÷…§Ë:ÁÔ~|ØÂ0{"ÉŪHbĶaSƒnÉ­HßsãÆ2¸§ĢĢËÔų->'E Î=œRāúīXSŲéSrĩ`:ĖõÍđ÷ûšōī/‚Ž×ëõz˛ÛėŲ¯WŊ­…Ž.Ŋž<ōOįQ>/ŧöëÉk@ąq{͞ŖøČŪÜ ō,x‡]ßĩ5EX´÷ČČՋŧ‡ķ§Ŋ^uûÚɃåŗWĮøŅú”ĐÕÕĨ×Sšī|8ŖņģŦ†‚ŊŠvŸ~Ęģž&l*:mĮĩĸ¨ēēēôz}Whúà ˆW%‚>wîÜūžz‚ú鐝â+9âîq@ Œ5Ō‘ˆ4{#=8oū΍Ē͎„ĮRŽqæ~;‘ntŒĀoĪiÍØCiÂgŧ>ę^ú­Šs'îęžuŲet-ĻŽđ‹ũ7ņpzŽaî4ĩō%Ō܌ÍfĻļtåog_Ņ‘1*č­ųž´N%éŪšcĨ94{đČpZÛ_e^3´žP(twwīų…@ 0œwnôg"‘¨ĢĢ‹Īį÷ųüŪŊ{ÇDû=Ãįɐ6ūäeo/͝#ëļ]‘Ķŋ\i¸ãĸš•˛jG™–ķ¯oVz˜ũ3“æü '{ØôÜÚz7Dŧ@´ļļĸ)ņŌOø÷îŨc0¨_bĒĢĢy<^ŋt77ˇ7n\ŊzõņX@°°°pssčôAŊÆŌčÔ^˙Ęŋ åļüiöćҨĩūË˙ģ3ęGm@͊@ŊņŠĶ¯@§ĶéîîîZ­ņ#Ę•JE4_LŨ?NúŽw-‰Îõũčy{Œ19ūģÉ˙ˆī_‘"h#¨ˇ ^*N"‘žz  t4ūŅ|<6D-GuHę-/gÅtš? ęD"čt`Îōđķāéåēö;UR&w$ŊOęŽęķŲ´ęN ŋˇ§9˙{ĸē–?~-€Āāqčŧôg&Đ_œ:ĩō˛šzË@!˜âĪgEĀ‹˛ÕŦ™Ü`Ž“‹oVœčæNĶf;=ÔßZš, uŸį(Đ%É ä8Xų<ģ Ÿ'CÚøWÔ^¯ŽĮ”J%j>Ô!¨ˇŧÜ`LG&ŠVÚĄ¨Ëŋ\.ÅuzŊžÂō ôq4‡Î†?. k•`bĸ'Xēķ}(eĨMšü¯å$Ļ×$ģĮU¸ŽõV­†5á1}Ž7ūqą@,’9Öĸ×Ņđ†ĸ‹%•N¯×“cüüŦ@Zx&Ÿ4qæ8+‚Aę_RųĖōĄˆķ/—Ët&&zĀü§ŽcĸĐ{_ni;ōīg×ÚxgPōyv>O†´ņ¯ ¨Ŋ^ĄļFzËНžž¸ŗQܤ3cN ËkĒŖ):Ēs.ëX“,ëËj)ŧĶė0Ž#`/›ē §Šũm_´ĸė'ŲöųZ× ,¨ĨO˜5ÍëŦÉÍĒšáÚ`ˤ™ŪtÖå^ŠŲ͜˜ŖG7Ĩūļôf-p&2AZX.wšÆ5ƒ¨k>.Ё@ CōօŸĢK{^ Į@×Q/ŧX%ՀNĨŠ X–6¤RáåBG{ËŪŪöé{`pÉÍ&’߲ˆÖĩ×IM؁ļ˜Úģ˛(ų†U’ŌˡšT:P)U˜RįdJįŒĻ+¯ë´ĩi*oĸ¸zX,KMÉåÜv{{–ŊÚšŽ:@ ÄKɃ=čEtKųårÜã͙ŽĻ “žÉ×ét渙ŗœ%’ēavŠÍ¤™Ū´'æŲYWŅHqō2ŽĄ }_uˆ JZĻ;Ņ Đ^ņĢĀđŒŠŠũh›˛˛ÚF•Xn9ÆÁLGNše+•HëĘ/•Ütž2Ō5˜  @ŧÄčUzs:é‰ @×ŅŪI Û:rĮņÆXjd­8L ×hŒįĐQ{Ģ•Îu¤?ŽÍéöLŊ¤ļ]ē֚F•á’×Ėiæ]KXŽëNŒŲŽvĀK/—ĒXŖ ;Ųņöv9ĶŪɕĮs É%*jĢnP@ â%†Āãnyņō…:…`BĸQTĨËdš“ƒ)Ŗ  ût9…Á›dßkĢy{uus ÛXx›`Įķ“\.Č>OĄ$ čÖĨÂėķæædNya'XqĖoUۏ2< Ē“Wæ×*õ÷@ôˆ(č@ /Ÿˇ šŌ÷Cķ‘A3û<kõØ'˜­wp˜ˇŅLé3g÷["f;nĘĖ>Ÿ™ÚûÛIÛŲ(Ņ0\œĖN`Ž›2ĩč@ ˆįŽĨ4įRĩ’6z"m4˙ëŊKW–˙ŅÚ"Ķéún"–VV.cÆŋf2Ŗęu÷…ÅGŠ[ëÚÕZĸ}|€û\O+ō=@ Ä+ÁĘcZ˜ōÃĀč÷!қ׋M^??ŋĀĮđķķ3yíĩ›×‹V„ū^ųũģíj’™)Ze{áų+?ŪŅüE{ĩ’]ə˓zũ[qčv(ÅŽō{Úŋí­âJáŨû&eímÁÕ_)Ģ&yQäĻjÔÕÛfĖ^+ğūuurØûɕø_ǜėô{s ĨČ'O—)ūÄOÕÕYGÆū¤ƒĸ$=ŗ°ųAŋ—)ÄūŲošũWōĪVí‰ās"ŽÕ<|[öŨœ°]"õ_¯Îßhž!IMęĮÁ‚ų˙ÆĒ:sׂŋãĖgeđûü ŗV ƒ—Ĩėô{=<0%6Ģš˙´âĖĩīÍ œÅŸ0{AėI‘ŧ{”‰Ž&DN™8;ō҇Æ|(ŋúYoŋ÷¨ëa@‰mÜČՕC™+~Čų&­¸´@%Iڞ[ĒzqĖSæ:—Zûb?§•ĻîĘ´˙RS{5~ŋHú|,éIËŲđÃ˙-?tˇũ™ŌĻŸ­;úUŅ' EËÎÕo͉‘RDü#ôAo‘Iũüüēēēēēē^{íĩ>˙5jT^^Ū¯†ûxŽppā˜¨ė-,Tko‰•š‘%ˆNd- U$¤)Ūûx¨îŧžÍyPf"ŕÂģDÃē÷G'Ũ¨­GRūÆ=˛ĶŠŦŸŸcĢã’ߏŸā„šSú yõ‰”<Ö>‹ÜW|=Žėc#twvč ZÚošƒW5ÄŗÄ1zwvô­—ˇqÁ÷N,2H˛bR…ˇ%äŲ/˜ŊÕY'q¯ÉŦG‹d|>cв5÷ųîø–€ÜpÄŦ‚>ųîĢĻŽ>ņņōĩŊÅ8╇ׄ˜Ÿ~˛–åÆ.]›âyhĨ֟ą×WŸÚ=Ãz`v=-ąņĸųB÷BĶņŗ&ÍgThLeĖYķŖXšúâįŸU Uō‚˙—¯Ŋ ˜čA_.ģžr}÷Ļ€oœŠ>ČũˆE wvvâø“"}*Õ§:°ŅJëątipˇ¸hj¯&œŗZņĄ3ŊöęÚSjŽžŊC#S“}xtŲÍV™VŨN´yī]/7 €ļ%7ŗė|ƒZ@wāFĪčŠÅ5ˇ˛ĘDjĩøĀš“DzØģžãuĩĮŗĒDmz ‡ķŧ>ōą"ĩÕ9uŗĸC§Õãž< ķČĩN™îJÂŨŲ{śOßē#ËŨ”°-ģZTYO_^ģpká]ėũ‡Wģc ŋ–ŋãt™Č,†\æ¸ãøžņu]!<öõϟo(p sXÅŖ‚ēbÛ{qYRĀ0Nđęo—úXȝĨÄo=-”á4CŽ;ā•û6gļÔ`ŸÎ9ˆ9FnÜMÍMJLųũ>Žã0"xõÖE^4YaRâļsĩ¸Į™ž1;–c›w‰kߚ‹Q=Vˆ÷ypVjÍŅ­'îŪĮ?~?—fõÆÆŅԏȃŗAėœÍ˜Ī™DÚ!§°p+Qv…LŪ"cÎŪ°#Ō ŋ–ļ~Įé*p`LūxÚ@cbHöÛ×ŊĘõĒĪÜüõņrĀÁÜë_qĢCAœš9ūp‰p58†$lôŽÚŗĶ ¸(ũÛ¯(R¨ãĮl\d ˛Œå ~˙äÖ €,cų‚sīŸÜ1ÄÎIvc—få×:Æmvë–Ē ļŪ~"Ōäמ~g‡ķî}Qœ—zĒQW§m+õZņŠ#kÆÖŨoTîZđŋe#^™ų|ã´máÉÂfū kuÅæwļ:îŪÅ~đzĮž((*Onūúx‰ĮėxƒųŠŦoĐÃvā ļáĸŨÁ‹KM“vā’ü"|ürk`øĖķĀ7]•€“ã3ķaMęû NÎ:t:ŌąŋĸŲs’’¤Įũ;š`xÅÄ}îÔk ĢĪü蝟cR›ĒÆ#‡KΓFåŨ9ø$Lą æ~Ŋ’Č`Í÷ŧģK=>~ÆĀvÕKŌ6_÷Ürz‡MqcĶŌ¯€¸á„CZD| €"w}bc;<Šęę´÷boôZžē9ūŒãÆ}[xT…p×Gņ[JđĄ9Dí>ŧš  (YŋhķąYĮbšë~ŗ‹;tyC]“öŪ§70—EĢC›šßšĮIúōüũí§'0%/]›Ė?4O|ÎjõŠ­^dĀë+jh ᝖ĻŊ“s*ÁĢw$Ûq^lDzŧbãž.(=ƒs[ŊhŠ’¸÷W íŨũŠ#Y–ĩpQJ~Čv>ūÛú7‚7Ûʐœ^øéļ<Ī Įŧoô,W~msŧĀk랯84 VÍߑ럀mÚW3g×Ép€LT†S{WmpDÛŨũ wĢ\ƒ3g^ļkísäį k\”ēüŗøėC{‚ûÍAzŸĩfãÉ,L]|€Ę{?K+›ŗÚ“å.17æeŸúeŲģ˛hī˙oĀ‹|gË˙DgŽU{׉ß%3ÂûéEęŠäOcĢweōPŸũŅ;™OËšŖđĶŲÁP9"VÄFņāųĩ´ßÁë+ pE•c>ø •)Ēj€ūî)áˇ7ŋ5kPY<ū⯖=9”n,1ƒ7w1͉O-ú‘‘ŊĮíäå‡BXT2@}æGīí* ŪÔ÷ę…äķÉÖL–šŠzõģWŗĄ&uŅg)Õ3]ŸšB×9îņ!P_žåŋ%ÅÎAŽĩÅĮeėĨ }™DTxy[NŖķÛVu¯Î:ÜbäŌ_Ļą•SÛĄ62aŨÛT3Pœ=ūuÛ¯ŋTČxA<ФŽē]ĒÚŖ!;üëŖ Ãt’#‡JŽßąũĪŲū@Uq+ČBw¯đn§ķxWŠązYsÃG #*û;§öųr-]+=ōCÉšFv¸-ÁÖßŨ 33åâįĢ9c†õ´PÛvúĀ}ú ūrhbҍĩížūßđ,H’]* ü‡‹ú™Qoē™ƒĻąü›S7Į/ôö(ŧÍ~oÁĻ9@‘{(÷¤Čaš3ÛJnĩjKÕ&ōŌĻöąl™HAmeF,==Ŧš•™Û˙bũL(ŧÅ Îõ Ė„ŧЎēVQ’‚`6čˆC 6š{…Wļ]j O›ãåö,GŅÜŌŪt[˛i;Ķž& :Šŗ]ZõqG]cņļk˜*ÕũXĶ^_Öĸ€ŦÜ Ã--T@s0mĪ)NÕ˛\l]™f ûS0e72k?HtŖ•JÃ@ŪwáĪZEüčI˛•ÜīR…‹7¨üÅ<*PyŗŪ ~ņ›÷qĮ%įoË.•Hq\ŪĸđīĀÕ-YBFď@ĻËPV’yS"ŨˇęŊ}jpd8s´Ŗúäļ/ž šėéà đĸüųÂ˜‡3 ¨ 6ƒA3ll ˛˜˜BĒumV~­HœđŅA\Žą¤Oߖ‹ŗ īV߈_~@Ŗs‰‚ä?Ŧæ‡Ä¯Å|ž§ß ƒÁßŪøđ!Ô €—} –âãc}Ŧs™Ã:(ɃûÛdEåΚÁcõr=Ų)jžÕG?\U|ëu°ÖkEãe™Qj’-8X 0b(š”īī}Ėz‘MWWœČĮ‚žsĀ ŠtJ>˜' ŸÃę§+–`ü ū šõ)Š’1cfPÔdlũwėVįĶ >´§ŨmXž(z3á¨đ§ē4m–ÜlŒ  –änūôë/œŽfüÉÄT÷(÷Ũyddo7Ķļm-Ŧē¯Āq‰ÂJĻčS_šƒ3ĒŨ0*ĶÁ™ Ā`›ãų˛>Áøg‚Їŗ…°GŒ7¯­Šhפ2åŪCM­ķN­–dlĨ P=l ę\zíâúķífė˙o@d°ŠũŸÚ›J•Ėšc¨$ SLöL$ m3Œ6A„TąF˛Ļ /˙oq›¯į¯"?ƊšŌãŠŧĨ• IDATįöŪĶq¸÷†š6`jkÉ É֜HˇŖĶ€HN֋ÕÚvÉéķ÷Ä24rĩ­ášĒGɝ Ģø¸‹ŗƒ­ĮH hĨŠ?ä+ė§ŊņĨCSŠŌjާ Č$ƒĩíĩ÷îļkNĪÉĐwjéíZÖCԋ Ë~Ŧm—ŠAĶŽĨĢ5@¤ŽˇÕžoTz´7ŅũŨ9×îT´Yĩ7]ČũYøČÃZşXŦ‰KõĀËv\qŖļ}kōšI\ŪČ­ŋĖŽD’ņŌ t­B˜uå`e'˜ŲŧîdûC¯=L}B&…?Ų†‡OėmŪ[đp›;°ƒ6Ø5–Õ6Ũ*.>™Į^ą`ôŸÛ̃ã€=1|öwĻ,#amēĶę삨˜äčĮ˙.3HzŠ€ĀÍŨVlŨÎīeֆĶģJōKoŗ7oÛåĩãđęÃ6*ôŊÁ°×ŖwėcĖi_ŲĩŠsĪØaôŽcūW ‹K Æn;¸čЁāįԕũØ8`ąŪŒtK9|úw§Ķęā-ŧ—gĮŧcĖ>AĖcĒ=}W.wŅ!wėEļ\!ü9Wz>ž›xŽwü&žegŦYq*6Đį"ëž0Ļ:ōg9oŪ]#‡' tųĩ”剅cūwĨaĶFåRņĒN!ÊNTōĘęŽúĖņĻÆWČÔÁŒŋž¸˙ĸûŲë:'ųãī$˙JܒčĘP_[ûÖŽģéyˇ¸V@0Ėîļ>ž_ōzHk­ÔČJĄęėųkĻį¤ž=—>č5¯?„§¯}DF°!ŊĒØ\-f8Ŋg$š˙ŗŖĮÕĪŖøN}č¸ûCúá!ž_:SImˇŋIk},%ÅãíŠ_ÖJnŨ‘žŋyŌÁ7a3ziHôŖÕD„Įvĸę€h;zE”Ŋ<^qø|ūۜ–†į˜Ã­_~; @îb*ģŲtEIō}‹5ŦŠę´¨tVatÂ,Øb=¨˜€‰Ŋš}‚ī×įj˛ËšK]n?\ßĶĻn…›^†xŨÅŦüF@¯×€‰‰ É{sLŨo%ļSƒš=Ī7ÔI ĪŌø3]éĪÅ´ÎÆĒZ×ÎtšgŗalƒeK€…ˇ ū ūvuuĮĢŽŊí4ûKī5õu'D?ž d ™ŗŸ1ûÍ×)$ū눈ˆˆŲSFšKK°3ŅīšIŽSÕUUÕŠt€Ø<ōė‹P‹ĖʝííuŨ‚ÜųĪԗä6ÃwfVɎŨZ"ŲÂwŗ×‘œDæLōδŗ—ÉVsĸüįĖu?žU´žPĻ öģoąˆ˛;{ŗŠ;D$]ųãGQ€4šëZ\ŧ6Ų„á<áËĖ'Ô1įÅqą‰ "Ė4*ĮŲWøÍظ¨pųŌādŒ5Öûᖠ™đxĘÉYAᎏ–CڄÕų_Įŋ‚`Ža[ã‚h!}ąãŖsJė ž_ģ42ŊĘd`r`ĖŸëüqlČĒķßūotâWõ‰›Ãf+€æ„ßΊO\+ÅŠdŒ1vŅę`ŊŖCŽ­}kv2ĶûĢãYėaŊņ÷éøš!4‡7žŨąx Í˜ą5Aļ~ĮGSļâĶ3jëã4Z¯rWė^´íë/ælÃ0†{đjž'.<ŧv}œ‚LÅ0‡7VĮš‘1čUĩg%}1÷E>ØúõüŲÛԀqøĢŋ foQ4;áßag¨4+Gj˙ģ„äá6'€!舿3^ę)ŋņÃ>oųWŊū“œ^ŸV%‘HaÕ;EÎlÜūOo~i.:Q6,bõÃCõŠôTlÎũëĶ1ŋ}Ŋh•Šą‡uk\kūę—W}’æÎĘ4ûđo ÷ąmËCļĸ´üí?}áæōūW‘‰_ŋ3{3Õ}îWk Ī\ņ!./=ņEâ×RĖŅî–u¨ Ž>ņũaɊā¨> c4qīŲ3VtQ#öv˛gLŒÃڏßĪĸYQ™T؃ C?Î1ēdÔf}ŋ¸sŪ`?› ēâÜņ §•ęN‚ÕĖo7 ĀXīĨ˛kĮŸûdgŸņŽO])žÅ}ŠCnæĨ„BĸŠšĀˆļsfØė:svšÖ„aĮ$>Z}õ÷.æ&duļkÉŽŊÃēÆfžô“B›7ūRü˜12ĖĨđŋrLÍIĻæz-<~!Ĩ—]/Ų›Ų d‘l93dxß*ŅvΛ6?œúm™ljN’j įĨ!ĘÔ,Á—j 08Ŗ? diÔ.ķÔå„kd…DzxųanãLŽąĄ€íWōŊ{.–f˛đo6Á_Áåŋŋ|īŅĘGZŒôcų'|ļ¤û˛|_ŠéXZq1[ŌÚĄĻĮÄ@ŽA×Z~ŠÆŸéjŪ^š\Ļ31Ņæā?u“Đ#$ūtŠ%‡Ō$‘Ģ4Žßf­°\"īË1'šZ:ëū¸ü‡XŽ0aŽö÷sebē–?~ŊÔĘdT*•JeÂō™4ŽŠĒ.+•ēËŲM˜š“ŸŸĨ{(īÉk]]]  GēŒīųÅš3˙õņyŌšB………oÎzˇĪ‡­w,mGĸ}ƒžáķdĀÆË¯Ž}gˇãî}‹9=?•~oiáŠÃũâ‚øĮÚk°”ëĻE_c ‡V:aČcCÍr\ŽĀhTPŋûhÛ°-?FūškAuÅæˇļ2úųgƒx߂å˛Õ=5ô+5„UW]ž5ß}Ėâ?‹JŖĄHšÆŠoNŠį/ôâô"S^IģxŃ˙Ŧ‹Šhę‹7žgŽč}ŠÎ`M"Ea|ۚî‹Đú†€ÄQTßǟ3OėčZŠÎ4~aū†‹>]cîiÜij°Ú+˛/HĐMašŖĨĒ´Zę4މuÖUH€H'@ģQ=N!č[5ČÛW c†ačn@ ÄKŒN^]˜_ĢÔPlÜ™æ‘ SG?éåËg~֐˜Ŗ}l ũDÃéö\ËęËgN“Ė8“XČíOĄß‡D˙č!Ņ—ÆøWÔ^¯ŽĮP[Ŗ‰@ŊečN_yH1t1<$j‚@ @ŧ8 Ž@ @ Ž@ @ Œņč!Ņ×4­ƒ’ã`åķė2|ž iã_AP{Ŋ:Cm:$õäÄčVVV?ģ–––AÉįŲeø<ŌÆŋ‚ özu<†ÚuHę-CwúBÍ÷*€ļ¸ @ H #@ c‘ @ ^đē‹Yų8€^¯“÷æ˜ēßJl§sÍ{¤ÕI ĪŌø3]éČoH #@ ž ˜ũ¤°ĐĩhüÂüm ēÆēĮĶ,ĮLô31GNC@ ņ€K+.fKZ;TĀô˜Čĩ"čZË/Ōø3]ÍÛĢķ/—Ët&&zĀü§Žcü¨ŊôWAo–AįwTe_Œ™1ɞĐR]xĩĸIĨ 1Fûųs­ē–Ō‹5:Ŋ^ob9ĘßĪ•‰^wūôæöôŽŗ÷ tĨP+ Ž@ 0 SļbūÁ!Lh¯ČžP"áLą —uŌōršĶÔ0Ž9čp]OM5Úė\y]§­ŖŠŽĨĒJeãÃÂtŌĸ‚jsŸ™ūLāEŲ—KmfŽŗĸ öĀxcQváMû™tĐĢTŦŠĶFš"˙š@Įqŧ¸¸X&“étē>_+++‡a؀ Q)ë›Û„į‹ß՛ņOŽ°Æã@ žĮ‰Is[&AÜŽ‡@cYjJ.įļ;ØÛŗėíŦzÅšMíĮ0ĘĘj;:nÖg"“ k¯Š—w˜fvÍčôzK•@'+ŋZ.éĐ€FŲAëĐLčNöH˙ú=ÅE(€ŸŸ_ācøųųŊöÚk†ĄŠčĘĒ}EĮīęĮdmķ6dĖOžU˙āĘīnföüõŋîŋ§ë'Ŋübޏ^kxŖŧ°;;YŦRm¤ŽĖH;û ļjaÜô¨ôúĘ–æŒ¨€¨ôæWf|¨…qĶÃSÅߖ%NMĒT˙õĻL ũ;ŋŠÔ —„‡†‡‡††F'ž­WˆĶWFMįx{ķÃWĻUĘ_ sÅŠĄŪŪ˃hSsz”÷#øK2ž0~Œ{F^™ļ2”ĪįđC{ģK]–ŪŗCÉķ–ô(* ęQ×5€÷-ån>#¨.îÎX¸='n¯°Haáú䴌(6¨Åi ŖãŧÔéÖ“s 1€MVWĻ-Y›ąŒķbØ[™‘Žķ&ەÍkæOˇ´lŠū;Îî Ā6æueJėAXyBŽnŦ|/6…wbĨ ę3VÆĻĢČķzįĀ]w:-t€Ļ?=ąąR^ÜæëSŋˆIK†÷ģH˜80zđJŖr×rTC}éZ˜MŠœWą-X2}æĪ‘64ËëÎŦĘž.•KĨÖķļîˆv'C}ú˛…;ËÔLŪ˛ÄÄpĻ`}R•Bą2l:™Ę[—ēŽ ĪK]™ū{~•”Æ‹Ųą#ÚĨ÷ē/¯LÛ´ū`^ŊČLŪ⤤pģĘôõq)y 5`.Ą+7Žä[ƒ\°dzđ\ÔŌfЏ1I‰ĄÔėčˆôđŠĄÖĐ|vŲ{GƒLå7¤Æ%­R€ZšDoŨÍÎ_š:/#•OƒfAR|RF™˜v4ŠÂ˙Āé8ŧGí¤Ā]œ”Î!ŋ¤#Gũ¸W›3ĸ#‹Ī&š3ĸ#˛ŸŨ€ –Lß9"œ-ĖČŋˉ˙ŪŨĐFy+#’Ü÷¤Gs@.Œ‹Hävŋy'~š¸Al™JÀF&›Ę2‡ĮĨ”ĘÕäŖQŗarâ2—MŅGķ›§‡Z¨Ë"8{ŌŖŲ^īLæ€ŧ2-aũAĄXMļcĒhkÎĻN§ †Æ<͐Ÿ‡ķâü­ĀÚ?Ї'æ7Ŧtá;4)-¸2)"v N H?‘ačÆJyZķ=6‰õžĸęĶŖ"Ԙ<šT*—JÉŧyÁX~v•\.•sīÜúŦg UÃē­%‡ÚĄĐ*ē|ßOŧi žúyĻeBĖëÍ'.îk@ŖUˇŠ™_Äûbŋôų$ĀYt5ūDŖ’h@áíõŲ4ks#EˆBcũĖ{-=~“¨-N”fÜSk,8ܘˇ9lmÃē­%æjĢBÛÖĒá=Âú^ŊHĄiS‘'FúE'õ_ MÅyáūëí-Õf~ä8oKå…äœ KĻ…ĸSŠPÁpÎ$bs~ŗVŠĐąƒ|?ķŖa k*)ųĪųÆf-,mįFxM˛†ģY9ÅÔĐŲĄŌ´QX‹Ŗ=<)}Kę¸ųx}5ĸ\គvĨV§ĄPC#Ļ̊cŽ)­ĄUĨiĶšÍˆ˜:œ­ˇ7î­iMHZ°ā¸ÄDž>‚¨ŧ°û÷‹,&ޝPX};Ļ"5īl3ˆz•5wž×$K€Ēâ|ņžĸ–6 YS4mÖ’į3k~š°ĮÚ˙Û €Nô˅=T˙o§ŅZEĨßgJęĩ:–ä:Í÷sīA€wÚîÄᝓ› ‚ØŊlÆ­Éûrß´úȗW ęäՅųĩJ=ÅÆ=ŲW]›;Œĸ•”ĐGŗ ;” ļ<ū¸Â‚K§Ë@3ö¸@ĻŊŊ§ēđÂųjsŒ@173Qâ tFŖV?éVŧFŖųíļpvõ+¸š!"kJČĄķ-*Žw_æŲņūķļ™9@‡H¸*S4ÕyŦëÛîŽÛĢۈ™ėOåĐ+ÁvÕĮ<ļļnKrE†„õ>ë9_įaœ¨­;y4š0.tŲīËŌöÄqČÍŅÉųá{ø4æä5'BØ42@}zTÔļüā=Ķ×­<!Xy:‰GĩÔr'jĮĘ$šxgÔÂmųá{ø=f&yŪĻåąu? ŦA^)(ÃÔeIą)°ōGßZ]™ēpI|Ɖ=Ąd`ŽŲēĖ…õéŅQ›˛ũ÷đķ’“ŽŠC—q >û`ÕØ˜DëæėåŠø˛Œŗ|¨ëËĒ0rĪE66AČßsv Y.LˆJč[;ī ]’r=$‘7ÔēúnrD@J÷c8Ž3į€Q¯öĢPĨ vq;Īîd“Õ•IûhŧœņåŠeķÜÉÍy)BNôŠ—T[O_ˇî÷…KĻü>vōXhā$ėā÷ZFåƒŋoŨ r§Ž:ú;øīār˜á뤪ŋׇ†ŗûIX–´ü y͏ž5ÔgDE¤?-gEūō)Đ8ūķV&Dķ %yF-¯R`Lęƒđ,ST=邯Z° hvŧāeëVōŸJ7–˜É‹ŽĄq™Š7ožĮ'1~ß“ũcöDš›Ī. ÛTĩ.-- âÔđ%)U!‰îĪŧ3œø~‘ÃIĐz{ŨŅũD ]#Ļlm[ÆŪ+ĨŪ^žDôũ€3&aõK"€ĸvKry÷äŠÆc<¤ŪKĻâDIgü–H TųG.¸ÎŠ $ģ ŋUR‡čęįĮ$ŧƒ>´&´ũžę|ch´}‡ßĩ^îoļ_ãgCÔ5ä&d5ēFRŒãd…)j×mŋ.z{râX3hŽø"õfĩ÷×ÖĒ-YęOĩ„Ļĸ+ņĮǜc¸ÖÂų6QS”zūx‰ŗ§ŸY_]öx}ĄnŽ~îgÁŪĀ[[jˆPéĶ#ĮyRĄõzÁĒ_DŧW6•Ŋøã6T€üÂîÜ#7âĮhĩÍ*ĢĪ?ōpĻt#"§EZ’twŗrļœoņ‹°Ö\/ųĪuęįŸ¸RtwĪįl”ôãm[VĻÄâí)ņhåĸfŌ ôŒÂûW ę\zĀLČĢxĢk5P )r`å6ģĮ[Û °ā¯™>!3 ŖÕ'ĖđbÜ´qOš+Zë”4'ۇr‰@wōvęČvÜ´‡o| ˙ŗŸa¤ö ôŪp‹|ŗUĮŠÍŧRĄâáeCĢäxf­¨YŖM›ĘÖČ5‘äėÅfSĀĘƒĒŠxū×x›ĮĨiĮdRšvd ą™dšT@EYęļõų•õroP0ĨFŽ‘Čė`>Κ Ā āĐōëåV?ĩ8[ˆ…ė ° šđ@œ*Äy ūÖ@v ™ĮNÍŽ’‡ē`Öîl›ĮgĻäßU‡ōãËS…‹×PSĘƒˇōhÜą´¤¤Ø„˛É<^?€ đ`ģisY†˜ķ!2Fc2ą‡_<¨0Ũ™rĄ1"Ŗōˆ˜ÔŅmuYbX@ƒĐˆWšũä@u åõŪ Dv‰žĮŒJɗoådķžĻŖ†âáīŒøāDú2fŲ҃âÉ;NGĶ„é))é)…ŧ•ôŠē2uY\epRjí…0]}ũh>ÆßÁ!yō<ʨBW‹3„XđVÃ(cŌžōĐģuȁËád2€\œ‘Ŋ2›‘ô”÷ō˟yb°ãr™  ŽlZË=Ú˙–~ĶÜÃŖŨtģĄßæ{|듄Æqá&>;“ãÂ`ÚŅÔųŌį1c­I–6ŪTQiŗÎ¯wčéZæÕ ÖîŊ͌ĸjģtž$_ĸTjõmjG•$ŸíšĶ `˛dJ¤QŊŽU\)îJŠŠ4Z0S–ėĘ"€9ÕԂBaI3K2¨ÔJeŅi0g'|Æs|`ĪŨ"iCsįv‹ oĒÔˆ$öp:S eË €J7S5whĄUÜØÆâúYĀfėČįĢD î3–•5LlŦIĘf5¤ŌÔėíwõ@ámgūx}­ŠÎ”–#Į„"gĻĢŗŊ' đf Q-ØTKŽŊȕ^lĸF”[–"nkRĻMkĄŌÉŽ~ÎTƒĘ͎‰*”Hī*t ęlŗÖh@'*iąđķpĨÁ’ũ t"Ų™Ĩ;û‹p˙X[WŽ Ī™2(=ãRũEđ˛WÜ(€-Aߚŧf—ˇrë/ErįŊĸ:ąŖúbNŠãú ?g„ú°íúG4‚o…A˃uAüŸ#ˇGDønMÃZoĮímé1? qüÃ5 =ŧ"sK65,Ūš#ŅŨZ-\–ôčã+Í@ĸ\}/„O†]æE3ŖR3yØīĖč.dp;qĸ,?_($G%Ĩ'žHâ=ĖōSJ'ŋˇõôįU| °ƒ?pOIIpĶÕ!;y´—Æ1œeéEËuÛŧ” uHZ› ėe;šxčÂÔ˛^@.Üš,.ß=)uå‹Ryš0M m€%Ķ3W9{ IDATā¸<[mgŦYqīŪŦ30A؝”Æá‡ģlJËáIŊ¯gČ4.¯zđ|ŖBŠSš4ōĘ꾊÷÷§Å•IÕĄÖƒ”ø‰Fh{š›žw‹kõ­ÚžĢ@SŅÕ‰ũĒėĖ"Ī8Rœ?zÂǎm-ĄípōUÖØíëÆ>ĘĸOЧ{éŅŅtbĤ^7lUĘK1@ķđ5I `ã=ųˆˇņŠÛ.Č7ąW´[ųg—6ÍãZ@ $īčYGz4´‘ú­?Œ™"ēŲX!–Ų[qöí)ŸQ{ÖWD tĸĖü­ÎĢæwĻBÅą ‡W}"áFaáü ĪY¤Žë—>/=hdT•ô ĨQŧ#§mK*D͗2+Žp|˙ķö œg&öæö ž_ŸĢÉ.k.ue¸ũp}O›ēzlzy1wšâ„äōķįyôšqŞÅ'īé@)ŽJ9ņGÆŊÁØC´Šœ?ÁŋįÔ ęT-xĐ*iiĶvOvf šĢ'ˇČÔLwkuCewøœŒŅ AúÄŖäeéŠéer2'xŦ<;-Onô …Ív|&<šß ęĘĖŖõœC„—ËÕÍyŠ™8/”CvđbwáĻõÂąŅÁl€æ2AÆå‡G¯LXÉ'‹ÅA qy´ĘŒŧfW ̤¯Ú¸1ęU2“õb)@sUYÃS´ē5?š_ŸŸÍŒžĮyyÅäФŋįÎni‹åv\6ÔõqQ UĄ;Ŧ|q.MäÂŖBģ˜y@ äålõ—ĻgT™ÆÁeõrP7T‰åd;Ž\QŠP7—å×wˇ´ē2#5-īą#Zä•yy†ßÉ+3VŌxîĖî1ģ3MĐ'ĩQĪØų`ÂôüfhÎObūvũUĸ>OPŲŦuŊ 5_ÍåŗÉ g¤ĻžĢ”øŅdŌOk>c“XÎé§gSSĪ>ģŖĒôJ-@ĢHT ĩō류qÉõ˙`n¤ëb?Ÿhĩõ*˛ëXĻ%@ŅŪđä°=—ĸ…ßpÍĨķĩ†C]:$ E’ŋŧ’FxŅ›s+ŠZuŠ–ĸ›O?HĮ’ck!š#lÕ蚮߹KĩuĨäÆX}ÍE÷Āq,'4„÷Ą3ĄÉP­ZŖ¨ān3ËaEßŅĒŗÍrĻ@̏Ûj¤˛…,Y,€ĻážZ@bĻ4—HęĩZyEw—%˜QIĘæÎĐ*D͆ÍĩƊ› 3ŽÃÔiã>™F×HĘÁčūvuuĮĢŽŊí4ûKī5õu'D?ž d!ŊˆxÎ<ēōžäRųm×!-(‡zŽkčđgŋļ9wė•É9fTĸ9U¯*‘9Į›ŧqoÖoĢųyŊØÍAæ­\ÆYš$4“Ƥ1iRÃÂO Xzpe?‰˜ļÎh˜I*LMNį‡ģĶh Iáq‰üõ@ęØÅIIáËļ~÷“0NpÂÖPk9.NYš,•Ģ™îŅ[ģZ/œ‡UŲEûw¯ģŌŧ¤Ø„*5™FĻrįŅsÄŨ†ē,N áŊŗãú38¯Ö¸!ģņ*đbŗc†ĻSiÖĀS:äąá|Ļ@žøåŲŪb¤Ž.1[—­_ŋ$,ŖNķ_“´˜ ÎNÉnhĀ’N1DW™Á{N$¸˙ŗ{ĄšķŽ–ąŖÖ<ėĮ4Ū<ž|Szå⸐e“ŗ×G„SŠd6ģ[mZO_ˇō÷e •Ësp3I^u49…Ë č†Vä§,•—ŋlĮJw2ČËR’Ō™;÷ļĸÁ¨g\'΋‹‹ā'ĐÜŖ×žšŦO_ļ,ĩĒĄA Ë#ō¸Ņ;w†ŗÕráŅØ¸8)Ž6Â?zgB ÔUiÉ) +BĸûčlŖ‰{N&ũ”Ōđäæ3:‰õëŖWâĖädāÎ{FWé'÷fQé€bÁķ¤<ŒLĢ ~šSĶA<ž7û8éķ?ōŌôũÄoÎ4Ģ-Grž ’Ė($ĨŸ´ēö\z&Ŋí7į—’[+4$ŠÅÄkīŋZK¯ ̚KöīÎN"Å5hŧ<íI*kîį3J’÷fŅÉŌv~$— pwQ0cõU ŗŠŋo֙QfÖŅ@šÖÚ-ÛÅJ-˜YÛé` `6ÃÅæXîgEd 3v%c9zĖ%%ņÛEfĸ%Ĩ[ÁÛxŸ#ēŋąČtW–(&Āöí‘Z˛j;ɌBļéžéĄk*)ųĪ %PH$ŠÕœ‡A9ėÁåŋŋ|īŅĘGZŒôcų'|ļ¤ûžė§¸ ^@^ëęęĄPČãņz~qüøqŸ'ü˛°°pîÜš}>ŧ}ûöë¯ŋ>ˆö z†Ī“×xš`IčŅy=ģ%ŦŒHáH‹b?=ĩ\MĻ‘ >}ɒŧŌ’†ú>įÜ^ę˛Äˆ8ōÖ+]ČČcCÍrĩ\NĻŅ@.LŒJbīLû“gđ¨Ë˜{ԟĮų„âáˤ ˙ø%Đ?ÕŦĒ‹ģs+BĻtŗØ}ÄĘDo  \|õķ,fÂĮ¯Û ÆõS‡–`NUū‘‹YŖƒÖ{›ũ‰_kļlŋ3ũŗOâ_œ %WâķÖ^5“ ‰ž,ŋǟ3 Ôņ^bĒĢĢy<^ŋŊ˜Á`ˆD"gggŖßŠD"Ô?^>ę3RŽs'°”6;ná&ĄÃ0šKčʍ4äž?Gŗ 9›æBFŽz¨ĢvF/Ël #3y‹Wūi™Ũ ¨âDīā<—Ž&’Ã_uŪ?'.ŪĨŌįDōŧŅ‘ä/8ĒÆ”äâRˆ$kg×ĪŊŽÎ•]ÍjV7¨¨Ķ˙Fų>,ߔŠû $ųų y0Ņ~Ō„a^ę/.ũFĐ%IYYYKK‹^ß÷đr`iiéáá1lذ\’vDdČ0¤Aíõęx ĩ5ęÔ[†îô…"¤/7O {{{kĩÆ˙f.@ RŠČ‰@ ÄāŌ¯@Į0 Ã0ä @ âyb‚\€@ @ Ž@ @ @G @ @G @ äŅCĸ---ƒ’ã`åķė2|ž iã_AP{Ŋ:Cm:$õäÄč]¤Aų[š-ƒ”ĪŗËđš˙ĄlüĢ8]Ŗöze<†ÚuHę-CuúząĖé,ߓpč6ØĖø2vĘ_8ŸŊŗd{ÂҰ™ũeŦ_˙?×Ô¤møūšŠęûéę9lŌ+&Ё@ CMÍ7|/T=RzVžü9ŗũ.l;kr>™s­ADŠ•ŖÛˍ0ĶĄã‚Î’ä„ŖĩFž°{gígŪCãOŸŖ=č@ /D;7O7ŽEÛR+üųPnã€ÕmyڞŒk  ÚØŲŲĐ@ŪPUÕÔ9´ęNŗu°ąąąąąĸŪSŦlllll먴!˜Ft@ ˆ— ;ūģQ^Ļō’íŽ6€ŧFŽâ•íßd4€Ãŧĩ1^4yÁö ??zÛĻŠ˛^ @á}˛ú]G€FÖ¨ęū^^™sō×Ü  Øņĸ–ŧëŌũ3mˍ“É9Wj@å†.ZÄ&@guÎ3 ɉV¯ûΎ sĄ€Ļąč§C?_kŌRl<ųcũÉzMã¯II‚ā~¸a‘ ŠŗrOÂūÛ`5ãËÕSúŊå•ŋū7Ŗ ĒI@ąqãĪ~wŠS¯øžŠĶģ1ą˛œÍßdĩ€•߇ą3m@–ģũËũF=āTž}ÃĪ `ãhSuåF‹–hã9{Á;Ūļ¤÷L@t@ ˆ—ÁĶîÛ÷sPÆøąIĀđ r€ÚÜJ9Č+‹āõ —^ō—DeĶ@%L۟v:§ ŧF [ 45'ŋߟuŖAAąãzēq­TōGâēåJÆ5•• @Q•ņ_Ą @͘ŗ'%ëFƒĘÎĶחKkš}y˙÷§ë5 Š˙u˙Oך´@up¤ÕŸ4ũÉzijN~ŋ_PÕ<_ŪëÄĻY){reûíS=Đtåp}yDmĶĩŸö˙Z¯ų[Å!Ž@ x„ļáÆĩU * :žnEژ)\"@mnš :Ģ…ĩDnPßÍå ^ÔlO+"´Üžv9ëįCßŗvsZ‘ @s;GØīÄ~ļ(ęƒEŸ­Žéšûõyąą1Ÿ.đ¤@Se‹FĶ”›Û¯ĪŽ 9ķŨwxT€Ą FŲTt­ėfŗhɧKx”ž†?ysGˇˇwŖBg†ū˙íŨ}\WŪ?üo 3$ä’ Ŗ4Š ”bUnŧ¤öŌ>ØÕ­uˇÚ_Ũõeû˛ēĩęļÚUV[ëŨúsK¯jwm]­ZĢW•Ū˛(•RÅÁHI‘&Á<@˜Iđū#ڊĸâŗâįũ‡/™Ėœ™œdf>sræĖŗ“5žD OķŨĒ”ëՀ|ÔôŠ“'>÷ŌÔžDd-;ÔÜzKĢģčâĐĶ„˙néœx‘ũÄÎūųƒa×? û.,Ō¤j…†˛ÆÂãõŌõDž}“û^Ņ[ƒ Nœē0q’ĨžļļĻŦpYŖĩlOΈ¸'íV7ÉÕ]Ũe)ׄJ‰x‘Ä—ˆČí&˛[DDĩ_žŋüˋsššŦímV ƒCĨDÄČûĘŠ¤ąë+Œ.§ēŧ›á:ūųĒĨŋNt4ÛģY+׊a¨”!"_Š\HärYíˇ¸ē;ĐĪwp'*~ląZ<Īe/ ‚@šŧŋvč#>l7VŅZ˛¯lŨš36ˇ›Č×_Ŗ™’ĸ’a׸“¤}åôƒƒėĩV~t0ÃD¤ÆIĘ~h>°{¯ÃMž“#ŽČįmõĨĩžũĩĄŠ­"BÜ|â%.—ŨåJåžÔčļÖ×Û)XzežŧR™ˆioŗ´:>ŌĻ;wđ&7Ųm^ŋŽøųø‘Ÿdčȸ)q䘘õë^ú‹}ŅįoĒŲöš5O͡ŦØļ\wŨ+GéWųÜã ŊēŊšŗšz6FNf‚ŽS7Œ+Y1ų•ī8–!"’†'dŧ0gÖ(ÕUjŌ˛į•įˇflŨœÁæĪūŗI[˙5J‚´~í‰ÅĨ˙b5ܰųųˆûy¯ÜøÂôXŪÛönęíúô,ģ§=ŋÆpņ/IėÂ/W§_m˙­Ë^˛xSiÃÁąŋđÖۓŖĨDä¨ūbõ’OĘI’^X~a"w,kæbzëËŲŅ~7Yۗ—põu]zębžĶų+Zh°88RhR_~īÕôPöžøD]?|ž[쏎8`üSCcÅwve­ÕÅKöÉæÍŌöšŠ§yãáĨûäķū­|8îsģ˙UT7zÜÜhžđŸ•ãÆÍ q\œŌí“ŧĶôī¯+ëœmM~ÚåĶŊ‰ĻĀsžēCĻâĸÆBōšœĐ{D€ ō­ŅÆüí[Ž“Ŋū¸‘ˆ(¨CDĄ&'ËČą›‰„Ú.ÚĪy{EÎįų_P$—’ŊąŲED!Ú#­“˙ÁZûåęĘúSSŊ;ųĪ˙gØÕžíLPrrĐ9ÍÆü%:ĩÄmˇžŽ­u§žúšnXœŧ¨ČjÜõéöƈļ˛ĸKb¸<4ėŦnãžĪˇ§e]tĻīhäøÁBi\„ĐmmޝŠM^ØŋģõrÍp•üĪ‡ÖžÂ&ƒ‘ˆäq‰AūAˇ¸ēÛĐ­sbbâųķįΟ?˙Č#\öoŋ~ũ<ØŊ5ˆĮ=5jJ¸\æKDŽ}ëķ÷ØČÖŪqk[-;uĖDĩ+Ö|udoxęD…āA>,ųŠįåėęæÉ^˙Å6Į ´„^,Á]Á>úĮv<Aä¨+Úō÷Õ387ŦMSu5§âɏsŸ$"r Ö:Õ`߅_œŪëØÖšœ\üãĒã_ą¤Ļ*n[ąâ„ˇŊ;˛û,+Oūķ‡oTąí5;^™ģäŗa[įDp'6-ųŒæ|š+š—Ĩpūė%ŸÄ}ūēš%SÎüĨKjM~“nļļģ(á*ëęœéģœ‡•'ĪyoŽN%!KáĸŲ+×T>ą:î~9H‰†NHų}čŨ8GøG'­‰Æ.Ķgö€‰ŗū˛¯˜ŌũMõûŠ*ūôŅûof öC-^ÜA>äĶA–Ÿ>ųéãŋ\-Ix /y—5 ƒ4ēņ“’/Ķ‚ãRCrv5 ûčĸ!˜‘jSG41Ô66{k ŌčÆ?—Ŧ ĸˆÉūŖhįžCĮ e$ Ņ]sĐB&xôŸ_Ļí{ eEF"_aP„61BJŒbü'9>ßSf,)thFŒũp1‰3ũŸ|6Žų˲æÚ˛z͈Q!EE]t~a"&˙yēh×ŪC†’ĸZ"_IPß8]Ѝ\_ŖBRĮWė/soPܓĶĮ‡2DˇŧēÛĐÛÚÚ8ŽģVģ„ËÕŊU#Ŗ„ä4}•ũs“ÍVi#ß ¨)Z˙Ûs4 é#66œ#ÔŽŨRUįņaÜ$ ×ĖxĒoßÖÂĪ ~V1ÆĶÕNųôq>967oiõIøŨ¸ß‡vœ,8ēí„Íí&M~*>6 ĩđŸųû”Rgk›ĶMáįf„ŨöŽ8–Âŋ/]“[㠉ÂĪÁ=Nd/Zō‡ÕúŸŲ'ūšiá`–ėeŸ,^ģû˜‰üT ģ%bm§fõú/Vīøų ÷Ę …Rų+Ö>cXú§u•‘"~ÎĸˇžVŗdŅŋŸšfŸ‘kį8eܜĩKĶŲKVŊėÕuö>XFšžŗî ‰ã8ęŧ0sáĨQ¤ŊfŨŗķK#ÕŦŲá0[%ãŊûzœ¤ŊrÍ´E9fb‰ØČ´…īÍNčŕ.za%Œū9ŋĐ øãģ#ũŋų""‡ŠŅ9đ­­=)1å~øÎēr q3uÅĸgúŗŽüų“W˛ ZLf§…Ô3V/šŲõIÜôÕĸëŊĩEŸŊ´„m}3†%âNdM_āxëËWšeS–˜ãĸ&‡Ũę™´|íķŅ~Dö˛-‹×n9f!bUŠŗß}ûf˛—$rÔËĢYĶÄ՟”¤žĨã įĪ^Yė –H3uÅĸЃ%Ž‹ įŋ~\[žyõøœMËS%Ddúâ•?•ŧđųj4Ģßŋ¸ŲßŌȡæ¨×üa§ūljz/ĸöĘ•ĪŽŽøøĶŠĄ˙ŋöĶŠ‘ä8ąså;ÛJë86DÁ5ŠįåŦ}âļ<ų"tTz¨÷ĸ=<^#ŲbvrDĻâ#ÜĐš ŊˆH‘đģXîī‡M¤Ž Uúꏟ8‘5}ÁM¯Ŧ‹ޞ.ĒßøÂô>ßũ|ÄÕæé—ėŊ*°;,6úņŪwlŽtŅ”%1Ņdq˜­ŽČIËß{>úŌ_ĸŧ‡M Ûhą˜­ŠĮ§ÄÛķõ?;f8īŊˇŌnßĩŲĒË×8Ũā$™L`kš7+ž/Åš€ČeŪ÷uéūfžˆ‰Œ1Eéī2Ŋ˙qEo­¤ŽĸšI1xųôŪg ~Ü\nĩŖđãíŠa˟Ļ_›ĀĪ]yJēĸ™Ōbܖc¨>×Á“o]üĖųĨ/^ļI1ļŖKŗ}f͊ôõļ4ëméOü>\`Ģ.ßxāŒÅMŧ¯dÄØ!Ŗ˜_ä˙_Ŗ$ŒZÛÚŨ6ŋŪĶž‹xŲĪÕ.SֆĘčŠŖĮ)ˆČs2ûÛÍâ„ĨŖŽ(JH.k~Néžē6ˇŸŸ´Ŋ]”>îõ>u9ųYUíäK$ 7lJ´\Ļ÷?>æ.˛ãÛÚ;ÂFŽ˜'š"õšNü¸šü\›ģÃWĄšüÔPØZ˜}lc;O$ ×ĖH´ˇo녆ķđ_—úuJ{—ggŪōķļlÃ1 Ob?_§Ī¸?ŒN ¸ųīÆ9OŨââ7‰ČįâøÉĄÅ yķāŸŽũ—ĖįÁiGg"ž[ļęškĖ Đ ĸ=ÍBmr—5D‰“_ŧJ i˙ņ/ößyšö˙ŦZõ[ëôÄŋޚø[QęŅ/ž6ē‹čž8u~âԋNœ|ÉļÅO˙Û+ŋ1uŲĒŠŋÍĨ˙’v|7ęB1záĒŅ7Rž!‰SĮžzŲėŨ]Ũ] č<Ύˇˇ_cIžŋ‘;XÛ­Įjš-DD> ąynËÆ{ĖÕ5Įœ’ņÁō 6ũQĨX@ä(üŧpguøÜDnˇÅ%Ÿ5up¤X@DôDğÜW°Ų9`b¨ĀVud›%töF(}=æ’ĸ5šĸ˙[BDĸpÍėšŋÛüīõĨûšTOßÖvĶWKWū÷îîĩĨŽãŸũIG-ßžå™ÅõDDŽÂe™….Úē6NŌ^ŗeÚü㝏øŨügžZėXņéŧū,q!s?ĪPIüˆNgΜ–ĨO[lŪĩnŸ|á×Ģãũˆ;]Y/%ō~ˆœEŋrÁēö—>XĒj¯|g͑ø7Ŋܟ%{ũqķ•™UeĖ_žĻ"{ŲĘgW’ņéŧūáS?Ū4¯—„ČQēėĨ•['lŖ"â,’‰+6-”ŅôŅ/‘iĪĸ?팝—ĻāN|ēä3Åŧ͛JÉQōŪĖw˛“7O–ūĖęĖx)Õ¯{áĩO*Ķ3ģnfS<~ŨˇÆFŋ8I1m“~Nf˛ÔQúYždō{ũ¨”ˆUŽ[ø?ŖTdŲ=íĨuŤrßÎ_šŖÉÜē6FB–ãųƛ˙ü¤1OhŸ3‘."~Ū‡;C,‘%wŅô•ųOlθ"yG¤ŋ¨ŪōYž)5CÕ^ŗã gōŠác:įjW>5a ITēԗߚ|˙6ĨsÕ[RRft¤|bh֎īLéOĢŽr8Ģ\÷ę6vaVvĒ‚NįÎ|6ûz%;õ¯NJcI9ü™yķ§ęēņ-°—mųŽâß g‰sŦōâ"ë¨q´ųŨ‰Úžęēē)/KՊkÎCÄ˙ûKÚyFņøü2ŽŦ:–œôÖÛÃd)œ;û/F~>#âŠŊū•åOF°§wNj—ãã7čÜą÷ž_ŧĢ>íĨÛÕ-ę\íúœ–ØÉc^eZĨ+˛‰ˆlUG¯8Čër)†ŋ=Eéī2oÛtxsč˜Y}ˆ<ŧ= jöœJ_j­úaà ņĖ™Iũ„ž_ ō˙īeʼn¯<%uŽîsģ˙÷”,=ei(CŽĻo*͏úëUÄ›´8C=ÂWŸoägD1|ĶŠŖ:3D@įj×tŒŸ:n ˜øĻŠU_W ũÃ`""?Ք§íí˗oËÛS=PךEL46ōØæ˛sŠŖWsžŅ/uj@—EšķėŖoŧ&ŖÖ>Ī/$"'%ŊîīOÔzęčŠũ5Ŗ"ĩ}ˆˆ|‚†ÍRSÅŌí•uÚáũ:‡ ÛO?n¨Īž™)ô،Íg|ų“9ĨGÇ.ūī†\%_}ŋ­Ē÷ÜA×û)䞺ŗÂą÷+-ēäåqÆŲõ/Ã-~;ôg~đĻķę "ÖĮī/ē…FG=2J ėGd{ÍĄ#eeųÍDōÄÔæ!<'=(5pˇFqQÄ,}=†\Ö}[ŠöÔÕdíSŽúī⛝wÛūĪ˙ŋ"ōņU(SŸ1BLäv×ém6Ú,íÄÛܲvžˆČׯŸŽ7_ڏę‹VŸæÖŧrØP¸vÖŌN"6$ÖÛĢU1Hî(qpDėMŋ5ięTÍĻ-šĻäĮˡ Ÿ:/‚ˆ#b:ĩŠˆHņ(ëhtŨ˜cčũˊ ‘bāmęšā¨ËŨ¸ōāņĶVÎaq°ę.mR¤NIøĮĮģëŌĻžŪô­tŌGƒ{~Į¤úu/M˙ĖHDôč”Īwŧ!ūna.ëGÔn*\ųę; Ô[˙u;›Co§öĘÅlō‡á,ąÉĪĢ×}vĐôôä.:W—[ĘĻ.Oōî#rÉu>UEú?ŗ'úąDŽú=Ģ˙4uôîĨ ×nno¯Ų27ŗzÜŌFJˆ¸š€ŧSĩ-œ1uđuįbžš)˙M‹~Ų̝­ėũųÛq—YƒÂŪ}p\oKļŅ1#ĸĶ Ŧ"BŖb‰HސČãUĢT+¸JK;Eܞ[hlÆ_šęQĄ ųû1Œ/OÄ˙råšĀ-8ÚČ }:Пˆ„ŠŅĖÚ*߇H Ŧô%"O]…U:tp?! d ]>’ķ§$ˇmãúâŖ­DDacŸx#üô1̃r +‰ˆ¨ÍMŒë×†ĢŽ6É7xÔPŋwš[Ŗ” úfŅĐQ}|Éfüågŋsہ=DDmn™ÍM2"Qp Â—ˆ| ĻÍŌÎSšm߆_:ˆČˇĪ°åST‘ Ŋé̚†Į†*ęNÕ)ĸϐí§+ŠrŲŽōú´wDFôë=~6ĶîũŋÔYx7ņööā‹Ãløõ ÉĨnk››ĖËöۈˆüCįÍÜZqNĄ‹‹ôVW¸JæļūģÎŲĐttMī!Qk;Ņ5CÁ•gg[syĢrŠV‘ŸPtˍiߟ. ĸøā!G›JˆčŨä÷|ņYtđM"*<ũ}Zčķ=âÍ[Kö攸HŌ7}úØā‡1Ÿ?05pįēĶ\ŌčāODB†ņŋĶŪÁ_gīh IDAT_ŧöV_čƒūkSzŨūâíįÔŗŸ)Ļ“˙ûíÎ.?“ĻęõßwŒŸĒŧøĻƒFŧŅŠiĄõŽW Į{Íæŗ8ˇWŽ{åCĶŦĖw3cíeKžĘ"o{üîŦŌâōã%š+×důŨ´đQâĩĨŽX:–̝KÉŠŌ˙gĶƒõGËv,Čú$cí†9jöjÛÛNIJ–=K—|Ĩ^¸6+9”5}ņƟŽu5īŲĸ• \ņáŪF;)’æÔšÜqY‹Ú-ž5"ųåaë–íúļņˆ)iîŨhšĩ×|ûŗdāÕmũËâƒÉĢ—nĐ)čDÖôÅW LqSĮqų$_ŅX=kžę!8ôEĖų4NįŲ­üT “‡IWZÚĶ÷åũʎ’]…æ3ôʔ"âœįüļnōԐŽvKŽãHÂvû]°Ž´%ŠĸW~\o§kt{Ų's3õƒ–~ôē7ā˛„3\ÜuNĸ–ø]c]ˇRÛŨY×uįQÄg¨šeG,'šÆqđ­,Ũš Y‘ ‹wųšĀmîôrKt›˜Ģž$ē:%ų*gĖΘņ[k ‘_Đ´éZšyãÕ7‰ˆD…Wkōm”Œįī};žÁæM ģd>Ī/—ÆÜDÄÄNų¯ĩŪpßTŋâ}uįbŽ:ú%ŠdDļ+‹r›]~˙ ķįõ_ę“1âh sŽvՖ–+Ūû…ĘRÆĨŦûíŋ˛ĸ„Œ”ÎŋQˇŪ`páģü4ošų„‰Ã–Žxg_}îąŗå1ЁëúŸsí-tI§—ßuģŋôxWĢiâkĢī§ Ŋãß9ŪŲ°ķëīžŸŗx}Ūâōw6‘OL\ ˙í\IGë94Z)ÛŅ`ëĒīĢiį×?+Į IžĐAéŖ•YôUåįŨqÂADÔn*ͯŧ‘įnuû­é^H§]ī|F_ŧúiRõš3;ļz{´;ęK*oî `ÜéÃ[g–šũō`–ŗԘ')X"‹Áč¸ęuũģ)ĒīV¯k9#éĄė|~úpá GDíĻÂĪŽpš‘Ēût4!GéÖ2՟7äؕ}`WvŅÎåI–ŨŲ5ä'Ž`ĮO;ˆˆkŦЎą!Ã#ėsNpDœåØĶ…OŸĢŪŗuĮA˕uúƒõ""Guöļjiė@兝ú“/Š,—Įrߙļē:#ķb:'"R% cfëĪ‘E˙U9›4\ucĩÍÕīŲē%ˇž;˙W[×ĨĮ‡.įqËŋ¸ęúo?+ŖAÃŽlšįėGDöĘ_œ‰ÎPK.;ė\o˙ëēzoŒ($PÔüsĨ“ˆø_mmî̜ |eCƒųŖe-­Däj)<ÁGunQbzGûY*LgÜDnĮIcû Ÿ’dĒAķîŖÖV""ųTCķˇÂģ>=‰UcCģsĒ~ QĮˆ‰ˆdáĄŊ›Ēö]DDîÖēĒ&[ˇĪECDÕĘö:{ gē.ĘWŖāËĢl<ÎzŌģyí­mž˛Á}$ ‘­šÅÖŨŪĒLŸKÉŠ:7QkSS]ģlh¯˙ū—3Ļ˜Ę›nüą/2eų#OD­Mgnų‘ Άm†­˙­žôư7O;vT_ž{Th2Ü]wŧĢFDš0:í67‰dĘĄI'ŪŪ•ô­Q~]´´ĖO*d˜ŽöŌ3úcEļvŅžīī#"Ÿ~é)3 ›m)Ûļißf"øE' šķ—+lôˋž™Ÿ9ũąB*Qp]>š–*}ÅKúšŗĶÖąĒAÃ~ ĀR˛í“’ŸŽ‘ę‰‡í^<%CūÄ{ĢįĖ _ōĘ 9RšD)ąxOŋܙœÅ™KĖœÄU ziašâ×^4Ē'—žkž˙Ú?”ü+ŖzëŌw8X)ˆ ›ŗ"NBT˙Û*ˆČR¸øĨBģƒķ âÍĨĪ„eŧ’ŧ`íĖgÄ ŠDB…\qō<¸vÍQĢäįĪFDlċ+>øŨKī.üpå‚降H1÷­Ônד_\ˇŪQDzZīĮϤG^Ŗ8EúŠųՋ3'æX?R$Í~WsšüįÎĩ‰ˆXix|Æüæ WŅāæ ZēäŲ—$JąBĘНē|čȉ‘YŽq“>”Ã\röō 2ß1sąISŪ}û~í…öȎcŊŸYøkWgIüķqŽ•ŲÕŗ^MŸ3ōÛw^š.‘°ĄŊ/dÜ^Š įũå[H­‹á.üä¨ŪúĪš¸‰#¯úõ™K8ˆ#V3ōågô#"ĮņõYģ•™3.ģĖÍŨômãvÍ܌5ŪVČÔž\0°˙ o=ŸųÎŗ“VIOyëMīī]ĻŨso1˜LfúËŗGĸ_\ņîĶĒŽkģŊfĮ?6™æĨMŊōĸúŠØŽ×ué!ˆēœ‡ŗWîXųáÅUŋđî›W6ŸsĻËĻoĩ8Ú%Ņ‹ŧ÷}Z:vŽsu•ęŊąSEpĖ´Ø6~œŊYāîGž2"’uq.`N:ôëŌeëx"&,včĖh†:‹ Œ:žNŋęÃãä'ëä!?Á ’ČW>yĘām9G–é=D$R„>÷TØoÉŗ‹M"†˜~I*÷ĻĄc•Z¸Ņŗ3Z7æäŋŅNDE䀙ēnŖŖ#÷1záîÕ.ŠęŌĩĮž*~ũ0‰äĘH7‘/‘"jbũö‰‘¸ÃMŨŨŸeqC§5ŨøqOD˛āÉOëŌGŒĪ)]ûqĨ›Č×/`Äeė į—āÉéAYßügŽÛGĸôŊå@“¨Júgņ/NlŽ ˆJT%-=ô]ė’>B5yî˛GΟ?OD%%%Qũ‡^úžoļ'$\k\!Ŋ^?nÂå?´4 ŽēÛwÛ ŧ›ēŊņöÃKžũ8âãO_î”2-ģ§ÍÖĪÛԍÁŅoĢöšuĶ2iE֜ūD‡éú-Ī,¨~ķ͎t’ûôËv6÷OŋĪ}æß̟čÕķîîyĢ[ÎŲŦTBDŽ’gŽéũîîŪ^šōŠÕŠËwų;ŖîĶés- ŋ\pO/šŌE/ŦĶ}¸áiÕŨūX]?|^trlę¯Ã,ō.#yĖeÅkĒŖ?­ē؟s=­nŋ/šJžú>?zÔëqū=mwķ­žŒ?ššļmĒP<íøå~âây!ÃņM•Ģžn˙ũâŊ]XųĶGWėWÎģæ8č]ĒúÅßôūß{̍ų,™ŲO2âž:|Y­~ÉVĢՈŗ=IMMN§ģęg \Q]]Ũõ˜ŽÕÕՁ Tâ­§áĘ5Ķå4’*mūŧȇąęŋzī“bgįiLÄäW_Ų­Āí(Ų´ƒŌ>ĐŨˇŊG¸ęĪ6™’æ'÷ÂwŊ'áĒ×Ŋú—lą,̌›ąbö 1Ōx°úŅ)īŪ•]ŪRRÆNž?đa~NYÛŅo .<¨¨õč×EÛyÆ×G¤ō”ęæ3ĩĢyķŋŽVļãË("ĖÔú÷ŧŠã›*Öl?m!ÆĪ/:!~ō}wÎ÷œŅ¯9ęā}}D~#Ō‡Eúūö "‹ßÍ<î)Z’đɘO™Š‹Ņca)Ã{X„äĒ-čÖŗgNž8f;gíč¸üĄB@Ũ°\Ųģ;—¤ˇģEäņ@oüCŸ×ÃScøŦņ…|[ÜÃZĐ{ļë´ ‹erÍ@ÛãîōU@,ÁWî?ļÆÕ˙Ē>Ŗœ9>Đ{‹ŗŠčČGNÍ_Ķdæ‹˙aPK÷ąĢt–eY“x˜O4œ‘J}ë~9ŲøP÷q{`ų  zˇŖđ˜;æ‰č˙’ŲöÕq¨t¸—øæÆ|tIÜ˙_Ž˜m¨t¸w:ęJͤ ‹ō%™ēO¤­Ą ũä‹*č!Ú-jx •.8FDÄ{Ο9æ= {<°ũžåļ”xģĘšsŪMôÆ?„đy=<5†Ī_HŽĨGž/[Ũ铲žËχy‡‚į+ßĘnøeD´Č>Xnsš\~Ÿx7=Đ˙ÂįõđÔ>k|!ߖúÎ\ĮØƒ•ŋ>¨‰ ‹#sŽŠúvų냊t:ĒāžU[[‹õlŪá&Q€û::  Üį0ęĀw…> Ђ€€]A€Äc;õŖžÂhį‰ˆ‘†k†Dɨt¸ÚNå•z´Š4r–8ĢĄ(?¯Hž!BÕ  ĀŨæąŦh’ÆOĐČY""VŽIŒ¯ûĻü¤5,Vf.üæŠŧĶÅ;yĄ&q”F. âš*•œlá‰|˜ ØÔ¤gŽÔëOZx" NL@û;:Ül@oir ƒ•—4—ŗ*ĄĢÉî!‘‡s)‡ŒMS[ÍūœRSäč0j8TtR˜8nbˆˆČÃqD\ƒžØ’2!Y& ļúÂ}‡j‚Ķ4bÔ,:Üvq„JėMí2Æ`ãIåiYæ,˙sß{ûá”ŋ>PģũŖåŦeī…3ÔzbķōŦše“igÖš´äƒáŊZOl^ž†ēūæ™ō˛öĐsKŪëīOŧ1gåÚÍÚĖYʂõÛ͏/|oŒŠŒ9+—ģ;õēaT)ĶRōÖļÍūë´p†ˆxŲs ‡0DtöāûĢļ”jįjÍyģ šš™SÂ"ž•gˆZ‰ˆčÜOÛ˛ļü2|æëé}ũųÛŗMŋņۘ^tq&€ģĐ{=ūzæãÄŋ[›ĩš ~áU§„>#s]Fm…ÁPQš—ĩ8/eÉ_'ĶaÅΌīÅ1ūūD<ŖŒĶߟˆúFɲiãųļÃgĖ´~eCD|O}ÎņįÎT´jĻÅ÷""˙ūc†)KÍ×ŋ|¨8ø‹ŲŊųũŋ1DÄķ6 0ˇļÚ›Uc’T …'éŗ§øšE˜K7gå՞#"ˇÍFĩ6ŠWj5´a}–;>N;<^@DġV|ļĘĐgŌsGöbˆˆQҡmÎúČ4<>N?(îN@÷b‡‘mßYqvŒĒ×e¯ôęßĢoüČÍû ļ4=9ō:ëô'jã‰7É´/ūĩĶm—gĪķ۟ūžd&ōvkw_œČ_ŲŅFÔ÷š…îm=L—”Ã\ŗcTŅėō@?*!BLm5šûã'$ ˆ¸úũßÔh'Žņ4ėßũŖ8LætrlXBŦwaŽĄ §B5&M#&"OSņ7zņcãcåhVŋãŨÛ×ä$Ūz2oWÍÅŋ::ØžD䱖•ķÚ´‰Qb˛–įæ”išĢu¤čpšTcÆF‰ˆČVNDDŦJúcąÁĒ"p ͤķģĐā'đīך:‘ĢŲÄ+ã#Ä"’÷͈ §Z8ÍÕzŗûČÔaĸË UjÔl~•96I\WeԆ‹QŅw% ŖāĄÕqIBīĸu\9@ē¯ĸž1ĐāR%¨XÔ:ܒ[dôƎF:ŋ ĐāÚĘaã'C=    :tč7¸ėyĸG°Á"X‹`,‚E°ČŊ[Đģøa,‚E°Á"X‹`‘{¸ô܀Ž..6OSáŽüĶÄ0"UšØxĩōj##ļĘÍ1ž˜Â]üAtŸô›jA€û‰Āŋ_ęø!rg­Ņ Ī6&mH×˙…%Ļ*…å÷T@OÄĘÕI‰aޚcM×xd˙ŪėėėŨģwī-ŦązˆˆÚ嗚=ŋ.ā1ëwg˙č}<Öŗw7yPwßo-čršÕp߲Z­7¸„ 0LLå-. ‘)ĩ)ã‡ąDÔÖP˜WR:V-ēbnå€~‚ü*slR°ĀcŽ2RäcJ´¯ßĶ€=ÍÅpËT^t˛Ųå!"WĢ‹mõ\Љd‘¤û*ڂƒš+š…1ąräst¸ņŧÅä$Y œu‡J[ÔcŌÔ2Ų*÷æˇ\ĨëŠ(l@ĐącÆ&W=P.F ŪčƒĐ#q֚âC Bõā`–<œG –ŠDkCũę=ËŲāá\yQšK5@Åĸī ´ ô žÖ“yģęī0‹ÚŅcÔr‘,2VõŊ>wŋXĖ ņĩFnČ5jņɚ°~č~Ž€ˇHœüĖs]ž" KH ģ|ZTÚ¤¨N˙!"jk2ņŠ5ēŸ# Ā=æą–øžĻU:āąHt?G@€{M ;1õp¯á&Qt@@@@tt¸G<Ö#ģw6yĘ5ėØžûGĢ÷¯ļSš;rkÚn ¨ŗŗ/.|a’Yŋ{oĨ Ռ€Ũ#E(¨ĨÁæ!"ŨhRsŗ‹ˆˆk1:…Ē ę聀qĐzJBŒē*š]$;ĖŦZXWcæ4bŨØ"֊‰ˆÚūĄCF qæJŊū¤…'"aĐāĄ(ŲoerM凚‚S´-ÚVŗ_ŊfÜčqõ9UacŌÔx°Ņm„t€ž‚UFˆ fŽÚĖM¤P…‹] -r6™=aR‘ŗæP‰S2qâĉFGŲKՎנ/6…Œš0qâĉãxŽĒq^(ŽÃY_œ§o‰JĖ^\‡(\̰WDDÎēĒ–@m8Ōųí…t€C¤VÔˇ´1F—´ŸLδ4؜ NąZÁqæSÍ­­Ž‚ŊDDä!“įZN5ˇļ؋r Ļëō‘ËXœ×œ0&)Ŧs×68&”+6XÕąd0¸‚T,ęē$…’žūtM( ‘2˜NMvģPĨŧ˛}¤Ú”ņ—öHņ4ųG&Ļ ‘ .™h%bĨJģšÁŌvYB(c4l^•)˜Œų˜R€jŋÍĐÅ %ôĀŠĢŽÂ$đ&rq˜ŌUSŅ$ąĘ¨@geEŖw8Ķln#A`”’¯Ģhpzˆˆ<Ôcũ1;ûĢķļōŊŲzŗŽ€×!`¤“Ą‰ķæķú—TxŖyŗÔœŦw"~ß3 'a‚úÉL'›š0ÖVSGá‘ū5u‚wSå!}•šƒČ'0R—8$DDNCnnT%tš8—ŗC9䱄°ŽS%-.NŸģ—ĒtŖ"‰:ė5ú\SŗĶERubʐ`ö×$ßPSĄ“Ļ‘§ŠøŊøąņąr<žčĄ '°Áũ”6CC›ĮjhDFĘ.äeOSÉĄ“ːôI'MĐIŽo#"ōđ.:qôčąiIĻĢŦq”N(T%¤O=D) ępĩtDĻfLœ4a0Օ/é-ÃĒ´ĄœÁ`õ×PŅ,ŒQ## @g ŖÔ;kęęĢLLŋpņ…´įąÕ›IĨQ‰ˆˆ ŅD0-õvŖˆT˛D$đvØ]]ÄE˙Pu0KD"e°ŗē.éû"PjÔŦąĘė!g]•=P.Fõ# ĀåØĀ~aŽc%&ą&LtŊmÁĨ3\Ĩßšābbdˆ::ŋ$‹ mލo4\Ē*u€]$jų]ŧ6>>ėˇÄ,E(Éd0ĩ×h¨į#¯Ū}|<<ß͛DEaÚ {ÉĄŠė§D÷–Û7‰ô<ĸM ]Ō(.Ö%FŌįėŌ ¤‘‰Ŗ"X"žËeUU•>g7# MHU_÷b@Š et?ŋ}9ū<•””čt:TĀ}ĢļļVĄPÜw›eĢܛß›ž†.ˇŦĻĻF§ĶĄnŽĮ|$ˇČč Œt~! ĀÍ(‡Ÿ8 õp›á&Qt@@@@t€FqčA<ļS?ę+Œvžˆi˜V§‹ē…'yl§ fĨ&JÖŊ"lå{ \ ãđLQt "j;U”WęŅĻNĐČYâŦ†ĸüŧ?:J|ŗŨ^WU#Pw7 :\§m'+š¤ņ4r–ˆˆ•kãëž9f°F ›|SŠ™06Œ%ō4ŠxØUsIDAT~S6.-JäŦ/.Ē0sžŽŽĄ*~TB„˜œ†ÜÜ:iãty„ĒŲęfŪnĪÛ[Á(ãS‚ųú‹ËOˇvųH#FÅŗ\ÃūŨ?ŠÃdN'Į†%Äzˇƒk(ČŠPI͈‰ČĶTü^üØøX9B>:ĀCĐ[š\Â`Ĩčˇ)l JČ5Ų=ÔuēP?&B$ "ǵŧ’UJ‘‡w’zÂØ–ˆ8˙ē†Jõ˜ą,ŲĘķ+hpÚÄ–<ÖōÜīKT’‰:\.՘ąQ""˛•{×ĒŌ†ūXl°Ē‡Č\CEŗ0éˆˆŽ“‹§K fžˆ<.™]& b”‘ÁlķšZœžŌŧŊĨDDēĐéĸ@"™:LtŲZ•5›_eŽM×UŲĩáb|ča— ]Mæ6’] ˜šÉ% dIāCDž S=""ĩĸ¨‚‹7>BDŗū›bĮC|Žë}”Cƌģ4ŧs-D]Í.‹ ŨWQßhpŠT,>›azJ@—÷Ķ*íĨ‡kŦxÍácŽZFDB)ãjrzˆˆ3""ępuøˆĨ,y˧LŽŽJôPī[¨ ›ËĢ˜7äÛĖVîÛ" ĶŲKÕPd?ŒérƒĐ‚ĐcˆĸRRФ¤hw9Ožg#KOf‰H ׯ_”›+ …B!CD$Pjå >ŒTØU@W§Cšģ+„ Ũ˜dmJW\üÍŽĀCBÕāQIræęW JM(k´Ä¨ŅũüF=rūüy"*))Ņét¨€ûVmm­BĄčîÜÛŠâ‚Ōֈ”ÔXå=ébbĢܛß›ž†.ŨVSSŖĶéЂĐ dQÉQ÷fŨķ‘Ü"Ŗ'0v4ŌųM@@€Û|q 6~â0ÔÃMÂMĸ耀€€ččp8OØĩˇÜûQ"­2w×~ƒķæĘō˜õģ÷VÚPŠčpŗÄQ‰ņšCåV‘Įf8TåŖMԈQ/ŒƒĐSˆĸã9EåaIÂŌ*ŸØ4o<įĖ•zũI OD Á‰ Q2×x¤ Ôäōttt0 mb’Z. §!7ˇNÄ8]Ą:!Æ[b[Íū}õšqŖ#DDÄÕäT…IS#õ# @7#úp1'?ΤīÍŅ\ƒžØ’2!Y& ļúÂ}‡j‚Ķ4bĨ6eü0–ˆÚ ķJęBĮĒEDŪIę cCX"šŲ[^¸VQqĖčŒĐˆÉYWÕ¨MD:G@€ˆčaÚ0Ą™ĶFzƒ´§åTsk‹Ŋ(×āũ“ˆuyHč2•lvyˆČÕęb[=j1ĘČ`ļsqlpL(Wl°ĒcÉ`p%¨XT1:Ü"ÁĨü#͆Č/™äŦ9TÚĸ“Ļ– ČVš7ŋÅ{cŠ@ā#¸ĸ42FÃæU™‚ÉH‘)¨ß;˙ĸ z.A`”’¯Ģhpzˆˆŋ ЂГąa‰)ZŊ>ow) <äž”ĸTFÆĒž×įî‹Y#^/u‹T‚¨¨CŽîįwÅ#įΟ'ĸ’’N‡ę¸oÕÖÖ*Š{°bųČŪĸޤ čār‡ÕÔÔčt:´ ĀUq …šúf*!éünA@€ĢbÒ3&Ąî*Ü$ €€č÷š˙į^˙Æ9JŦVIENDŽB`‚django-q2-1.7.4/docs/_static/successful.png000066400000000000000000002277201471170400300205570ustar00rootroot00000000000000‰PNG  IHDR”#ë?ĖxsBITÛáOātEXtSoftwareShutterc‚Đ IDATxÚėŨy@gū?đwČE D@PaEąRŦÖû*ļÚ­ļĢ]ˇv­íēęZÛ¯úkÕ˛=lĩ‡­GEĢU[ĩ­GETEâ‚V”C@N 9į÷GÆzÔļĸŸ×Lō<3ķĖ<™į3Ī33,†a@!„B!äáÆ —ËŠ !„B!äaŪDFFRYB!„Ōy—4 ŠũāB!„ŌŲŲ.‰b ,í.b °ĀëŸöR’‡= o„B!„<",ÖØaZã8€Å‚õ%ĶōšaĀj‰€EI:K Ū!„ByD0 X,ÆÖügXŦ!-`Û cũ `(I§IBÁ!„B! cmę[ģmĀ88Ƅģšļų‚šQ™Vdp  peŠŽÕœR˛Ãzwņãš.į×Ų]Ŋœ]Yϊj­ĘÔŌ…ĮØ" Û[´-sąŽųc(ÉīžäÁotīJōĐĸķB!äŪ°,°FpXļyĒ]Ŗ ,1™KSCSš‰ĨŌ™-Œƒ­o‡a,`{¸;ËŒĒĒĻz,‹õ’Ģ–k´Z¯ÅbĄu.Ã,°ān“`Q’ûJōģôŧQ™<„:œV(..ūĶ) €ļ !„Bdđff`ŊŊ ,†ą˜­“M×+ę¯6ˇ|‰a9ē8ûēąUĒ’&Ļ%dāöí-•9āö÷î ƒüuĩ§žŨÄ2'6hÖj ¯5”iŽÄ=)gÖjëXŽ2Ĩ¸ :ŋ‰us Z"ÖƒX`,hˇ` $÷“ÄŧĩšnŽu{ =é›t vT//¯?qa8ĐŊ{wÚ(„ByÁÃØnXØ:"¸!Ŋģ†KĒŌë,-M"Æliši†Åx­L#ēĀ\QŽR˜Ė †ŅÃ͋c(+i¨…cĪîÎ}{ )WŠdl'@Ą.¨gÔ0 ͞l—rŲX¯Ųjķi˂’ÜOέ­[kN&“éȑ#gΜQ(fŗY ôéĶgâĉ"‘ˆÅbŨ_ŒGČÃËB!„túāÍŌڃcíĀą ĘĻF3F­3Y,,‹-B`,-qœÅbĒSj›ü….æē:uЁÅu•J9x~ŨĨ~Ö/qø2>Ŗ°ĻhĒ?sEŖĩ]¨uŗ‡ČyXopo?Úõ.ĩ^ÛEIî'‰ũž7ŖŅ¸e˖ŧŧ<‹ÅâããÃ0LUUUffæĩk×ūųĪŠÅb´īŖŖF0éŒŅšÅbš›DmęĪŨÎį.“PÅ!„BȃŪ,`ÁzĮBÆzŨ˜Ę¯) ›mÃņĀ8ØB6‹Åliéãa`ą´\uef, cļž1hÎ^QkÍ,‹Ņk,f.˜ Ļf–›ˇˇgĀbĻeĐÃXīĩXŒÅļ mŒ’ÜWݍHŖŅxäȑüü|ŠT:yōd???ĩĩĩ;wî,,,ÜšsįĖ™3š\.ĩAÉãŧĄũƒņ“PÅ!„Bȃeļ0`ąXhiå´tŦ1ŒÅlļ­w907ƒ7Æl˛LãĶUdQé+4u&§Ž<į ‰ĄHÃ8: |],yy }ë˜KKË%uļģÜÃö,iÛ\,Lëkëō´]0Jr_IėôŧFë­&MšÔĢW/ët˙éͧ§¤¤dff&%%yyyąŲėÖÎ7jƒ’Îŧĩŧm<ˇūŸ ū{´°ąpņđÄ?S>I~0sl>÷rÔŋûųųO Ū!„ōo Ã˛0°>)ŒaZâ4†ą0‹í‰b ËÁ%0 cfŦąÃX,sķåë:Yw‡ˇÄŖ‹:-ëÆ™ŧũ]}ŧ$Q€ŲdŦW4ĒMsK–f[7_k†ÕúÜ2ë@ƒ–Å`ĩ_0Jr?I:o ÍƆ†ooo___ëtN§ĶéX,V=˛˛˛ęęę<==¨ J:uđfíyküaîÄO\>>|i”Ÿ#šËÎũ¸įĸNką8>9Z, Ær›>>Ē8„By°l×°YŦˇĀ``T:ŽąŽēcĩŪžÄbšž_ēÍ6PȔ•YœkĀĀR”Uî)CK´–J•ņŋFĀÚíĶō ˛ÚĘmĩļόm.ĖÍXąÍ\Ú$aZ^X(ÉoJb'xkmg FŖRŠ, ›Íæp8ÎÎÎÖ&¯ŲlfŗŲÔ%:xŗžm(+Ķ{ˉôå3 žo˙ņķû Ã4—íY2oõҞfĀĨΜuŸĪéãXļ}ō˜ÅŲŽü†ŋņųēŋ;Ö|•ŋˇ˙Sü´íieą;‹wõ>ēøÅ˙Û{Ĩąü>ã×m]æČčk2۟õÕŪ/Öē$ŦÜšuNGĒ8„Bų˜-VK3Ŗõžķ-7.šŲō°† ,ÛĩV $%‰āaGGĮ7n”•• 6›ÍãņØl6›Í.,, ‹™›7Ĩ6(éÜÁ›ī¸WžúāĨčÛ‡÷īŨģlėȄūžŽhÎ{wöߕĮū×ߍ§æ=ųÚW#Íņļ6ŊĀĪÅ(ÛôdŌ’Ŗãw>Å@ßXVÛ{í–ķŸ{¸¸ÔŦráÅņߜ˙žcsŲŠS ŖôžĪsūs—+ď}û贝OšPÅ!„BČīÃbŲöV“­oqķ˛|[OZkā@I:G;Á‹Å ĘËËËĘƊwtttpp`ŗŲ‡ŽĒĒ puuíĐî¤6(éŒÁ›­“Ų}ܧ؃.žN;uęĸü‡ˇW/\2moڏŖ?^ŧ‚ׯūč šQqĩÖŌĨáÂûK^;}ĨŧQ¯/¯õhĐY,°đ=Ŋ8eX #€ęö^ {}jĪbąđ|b†ų 9›áû&‹qįYد7OuŊÅ"ĸŠC!„ß+xkébaĩ<÷™Õro ĻåĄbXև@ˇ|‘’t’$v‚7‡øøøęęꂂ‚ōōōž}û˛XŦ‚‚‚ŠŠ 뜝)x#Hđ€×%4~Jhü”ąXūÆāg6åŊĩxØę>tscķ…7ĻŊ^>o{ę†Ū.ͧ˙6xcąXŸkŲrQ›…aKÛ+Ü,Œí` `áņŅf\2UB!„Ÿīķâú/ŸķaāķīÔwŪü×_ŖŪ̓qĐØˇŋLŠYŧØg-bw~Ŗ-k ß hö—o•ĪMŽZĢøAcß˙ō/ü›3ížjgy:DšŋßÃÁ) %Ą$”„’PJō¨&‘H$Ôä{„ą†‘Ëå=zô°sŽĩÁd2FŖŅhi}@`0ärųž}û,‹Åbyîšįbcc9ŽÉdjÍņʕ+‘‘‘T˛äaĶv?pãÆ >Ÿ˙'.ĪéͧŸ|ōIÚ.„ByPnܸAÁÛŖ­ã°ÉÖŽë3¸9ZîbÂ0 ‡ÃéׯŸŲlŪŋ?€]ģv 0ĀÁÁF‘Náļ×ŧ=ËC!„Bȝƒˇļ1›õ‹Å˛ĄlÛĐdąX‹ÅÚąvčĐĄŲŗg›Íæ‡ĄLČŨ°7lō!ZB!„BîŧŨÚķf‹Åâņx à 0 _ŋ~|>ßzÁu NĄÃŽzöėŲ‡jy!„Bš‡āÍh4Ū1 ŸĪo{ąĐ­Iär9•,yȃ7—„„ Ū!„BH' Ūāđ-???*VōpĸaŠ„B!„‚7B!„B!ŋđFÃˇ!„B!„‚7B!„BȟÍXļsÅĮōæ›ą€›oxü„ņŅŪ\*›ÎŧҰIB!„B—Ā+4ĖÍT[\XU.ßģE°h¤J' Ūôz=•!„B!¯ø)ÉuÎû+ļWA]Ģ6‚söũw÷WÁ÷™Ĩķ"D€úĖû+öŪ|{“:Īæg˛9nĄq›3›„ąķq¯J]ņqnŗch|¸:ûlysđ ËgõlÎ˙iįOg 뚎[@dŌø§#<¸-Y{M^új‘ą,uÅĮšÍ_4ÁĢū§÷ŪK¯‡oė@nŧ¸Ūäč5pƌ aÚ^ƒˇ€€* B!„B:¯âââģüfUúÎÔ\SmqˇČ8o.áqžûw•—g\VGôŠ/gWˆëŲ.rƒ2cã–ŗU€ŖW€ģúĖÁrS‡|› ŌåîÁÁwąlĪû[Îjˇ€PwuaaņŲíĢš‹f†ŨqáĘO‡Į /<•[uvˏŊGá[‡āMĨRQYB!„ō80UäVY_:ēšq@6,xī–ÂōŒ|e˙đ2y9Ā Žë)hģœŠā˙÷ŋôāÖ{įŊƒõí3v‹Ÿ?¤0^Ū,לāķg… l×ÚģŦ l‡ÜĘ}ü ÉŅ(=*ßŨ[WuĻ@GŅ[ *B!„B+žĪ,÷Ũw—ž0PˆæâƒŠéĩF@æTe”ËËN@\@ûØÍØ\Ģ7?€›ˇ{Į|ÂŦĄ šÕõ&"owŽ[€hĒtRoY6Ž››DnnēZ‘6ÚGŧl6›Ę‚B!„Į‡Č/Ā g5PWÖāúŇ sĪÖÛ÷“ÆNhœ_ûØ \G A}™aÔ×Ũ[pZúĶEnT™Ô•õ:xŒõÅjz Z“Æ@]WÛÜ!S]e=zz ž˛Î@ā!¤[av Ū¨ ŽB!„ĮBUúÎÔ¨Ë ĘĀ=Ė \ī¸8ˇŗëËëĮ°Žũn$Ąq^û÷VÕ§ü~™7*‹ëo?n°HaÁYMá—|ęŽ.,ԎĄÃzŠĀņöĸJSw$5ĩRP&¯ē%iũÁß/öCYa=¯čP3yÅl„B!„<^LUššÅõptŽ1#ē%>ō÷8öčg§ÃK=kÆ@/G4W•ÕģE'ų—k÷ę5ŽßĶŸėæX_\PX7ßČÉO\īSz9ĸš<÷˛)86 cjN@R´[]qU3Ŋ"Ÿ™A×ģĩÅbF.—÷îŨģS,îž}ûh›‘?Ԙ1cZ__ŋ~ũˇdÕĩkW*OB!„ü~Ž_ŋ.‘üÆXG™ąöŨũuŽ‘_:Å^ôŖÎČpĀXųĶ{¤×ÃwōŌyũE`ņĩ?Ŋ÷^z='ø…åŗzŌPIģ8n‰###iŗ‘?†\.ī0ÅËËëū˛:pāo„By˜Š‹ÎdįæĻ×nŅņ^öŖ'ĩüĶ÷3D=ƒŨ5eōÜzĀ1tX˜ˆĘŽ‚7BB ÃP!B!äQdŦ—˙tPŪ a@Ԍ'1~d˜€ĘîŪčn“„ØÕĄjX,f€u¯Ÿ5 Õ2B!„<ĸ~SŪzwĘž$č9aŪ&üN ā1rŅģ#iCÜEđv'ĻÚ;\š<Ž×íēEM W ęÜC{¸r áÜîÃēÁ†xP×y”X, pow‘D_{!==_i6Cā7pø€ģ€ ¯8õĶÉ2[Ø}øČR[]ĶW9|ōjÆY⌧|Zj ēøÄŅŗe:°Ų’°øøžüÛNŧ'ĻÚģNēŽœØ×õ.¨¯å_õēĢY™Ē~ŪuÖgė¸^N´įB!„´o,Ö¯v&čk.\įu•čōKCÂoĶHŗ¨Ž\ø…Ũ+؍ĸŪÞ0ģpY,*bŌŠu¨7‡M6žûrų[ëČĢÕ|—FŊ°hQr—ģĪĒcœ’u4Ÿ;1š[‘ģ˙Įô_<ƄÜ)|3ŨČ/Á_&N iÛpDū†…°¯ũüc-‹Õ2OõĨôĶʀ§ž —š¯Ų}4ËkĘŽŨ‰÷U>,ÖŨVuSSɅ|÷n!žŽw•9î)sB!„ Ū ¯Î¯„ŠŠũáø/ĘŪ1Ö3üÚŌĖcgŠĖ[ĐmhRˇÂsÕ†ÆÆũßūį4T”ėgŨĐIC=8úĒÜcĮskÍ[Ú#vXŒ¯Ô÷ė-tõqŌéôZĩŲ#:i¨ŋ“IŸēęČ"99ųėŲŗ­o˜ššzĮˆËbą@ķšÅOLÎūņޜQ~Ž@sYÆö÷ˇgÕOûÕëu­–™ę¯TĀg„#‹….ÁŊ…˙ģX­ ˇ Ę´Ĩ™GÎ\i4lđ„aáMÖáS× å{¯xōŠk ģ¸boo1ÔÕĢ5îŅV˙ĸöŅ…Ë×'ÂG~Š7{ íMlŊiË3œ*րÍ6ŗĨFv‚ļ<ķØ™âĀv‹įīÔģą ¯Í=qüb­€Āk@`WLŠK'ŽŸ¯ŌāIû%=!Ŋx暎ąâСŋ°]ÃGŒfߒ! -=~đÔ5[ ôđ€, Ū!„ģWTTÔöm`` •ÉãŧiË.Ԋú uķđ ÄO•Qņž¨ ~>Qá‘4y´'ĐkM|'i”×ĩܐącųĐ`;enŽ:•v‘7yĻŋ“ž0)ŽnÛųÖ:×ašŅTĻfĨ-ņY,Āu’ō Å:‹™cgb›eT_;Wė7yœ/0éMK]pė”&dôŗ"˜Ôŋ?Vė9ē[ËĒJO¤UøŒœ–čʁļčđž´_ŧ'†¨O"ꂡdčUuėD­wŌôO¨ î*6SĪ!„BHûāíר‹ō¤ŨœĀq dČ­Ō'úĸöŠB1ÂĶÚhä;qŗŊ´ĻÆâZø$ø8āûöä(j0ŠžG°Gä-0i)„ IžŽš‘?Fttô­oŒŽŽž›T ÃÍeW<šßzëÉæŧˇg€e žōlÎÛ0vꋩĮžÆķˇ/ŦŠĄH!‰ķæpęv­Ņäíōģ@ęÍ;—q$=°›¯7oôĩWĒ4jíO;­ĪO0ƒ­6˜Z—PQXĨQ4ŪsŅö!xÚæÆâ*tKq€Ãį0ĩ™ƒŊ ›E AČPOQ`osrÚU !„BÚoŋrjģĄøĸŌ 9žíËã`6#ˇÚāį…ļc˛lYØ2˛ūoûēåü˙ÍwvK§ ,ÛĀ(ŽXFÁ"¤ÖΡÛuģÁū°I‹Ž™Ņ73‹Å‚Ægú[š z-M;6ōh†~Кxw‹Å 7Ķwõž<íTwĮ;Ö2pDŽ<ƒRgf‰9€QŖ1ķũÚ\2Úfpbk>,Ö¯]Ö.×IĘ7WiŒ,0锞ĢĮîÄ6yqeƒĻ>ZY^^Z”ą'Ë{ä”(€íųÔÔĐļcCM5ŦÖųƒ‡OÔåæ !Ķ+mĒ›Bh­üˇfhĒ›Ãą%á´üPPĪ!„ō ŠĒÖ~uĩ&¸ĪĒ‘ŽÖ'TŸĘū¤)ø?‰bEË zFöÃÉ៛nē~a–Í3qފœR-ß3XĒÎÉŠŅôZŊõ>čfƒĄC“Ô5Đå+´ôåyEi ôv}φōŌJí}ŦCŨî‰!C^˙ŸūO{K:kįÛŨwģYƒ7‹…8ˎöÔŅk‹Å"Jú*īÚĩË{Ļzč,Z Ŗ‹ ÃØØŽ”ûĩŗ(Ō`_”įThĶÂ‹jiˆˇSûŠ$ÕÔčhKsĘáéīz/W‡:y‡¸Ē/Ū0ڊœrø†H9ö'ļŠę-ÛÕ;¨Ī Á‘RC­ÂĀ÷ –jräåZÛĮ55Úļ+ĐĶÃP(/U›Ā¤ŊQŖ6q\ŊQ*/j™Ļ5`ķØfĩŪö2ä¸ēj ËÕ ¯*Ŧ5˜h?%„B0Ååë5]ÜW¨ÛŲÜĄhR\,2øŋy‘o×Ė‹Eęž}‡7=ē}“™Í†“˙đąƒŊ="qt÷VšĀcđ¸'[nIÉņŽ|4}įĻt€í<<1ˆėÎKSzōhé gĮŨķ5oîw_šØæũŸõ–tBŋŌįf—u¤¤û“KĻŦIūëë>_.×Ã@cMc3ÜÆ'&†ŲŧųtMŋaÍ÷n.›Æŋģįzsŧcû>ēsS:Ø"˙¸ÄĐöˇ>q _{øđöMf€į‘īÍi?ąMĨ­9šû§BÁlÆO›7ō<âĻ<ŨĶIœW{ø§¯sĖāyD$ÆX‡%ڛØJWyöĮŦZ°]CâĀMLT§ŸÜšÉĖf›Áķˆx2Ņ“×ōuž˙đ‘‘ééß} ļliāđ‘ž"QĮ~÷uØ`K#Ÿ~*T$ túéĀö"ž4ōé§ėdč4<Ļâ§ũߊœx<žˆGw0"„B,“&#ĪÔ71 ĮМ#Ĩ†Đž<*’N„Å0Œ\.ŒŒė‹ģk׎β¨ä —Ë'OžÜúX ØŪԞū|Õ{[æ×|žģoŋaĪŊöÚÔŪ.͡üķŸīŽøAcß^˙v’‡õëmŗ"„ByāŠ‹‹ëëëÛNšõn“ÆĒ+˙wĀōŌ_{Š‹.ü_ŽûĻz‰iØdįA§ĩ š7@v‰ž•˛kVJĮOyĄĪ~|čY;ß'„ByÚ2Ĩ9 ÷öáūŪūiĨr•×01 o„<ÚÁ!„BH§ŖW+2*‘ķzÍLMžfXŦ †‚7BAww!„BČÃHUZyEđ֌ŽR€ąęŌ˛×+ö¤€‚7BAÖįÂB!„tBÍŲ*IŸ`iË{Ž{×p\<\myŠĘ†‚7B1T„BéœŖ§Įĩ{>G<íÅAāĶ˙m€,Öö‚Pđö`ČårÚl„B!„ ŪjtŗuB!„BČãɁŠ€B!„B(x#„B!„ōĐ K!„ByDR!<¨įB!„B:[Ī[qq1•!„B!„<ėÁÛyzUqq1=‹Ē„P#÷ēŊŊŊŠ:ĩĘĘĘĮ­&>œ?>ÔķČŖa“„B!„Ō Đ K!„By<Ž˙ŧOîØÕÕØl06YDaŅQŨÅlŗĒ0ķLaƒÁbą88ųFÆūÅKsmÆg,îŽæ&¤Ŋüš Õf0ĒÕf˙ácĸ¤†Ē˙É)W'īŋÄöī*0×füp2‰ąŠŲØdt ŽŽ vcS‘˙Á›Á`8ūŧRŠ4›Í>bŗŲnnn‘‘‘<ŠB!„ÎÄb0Ę ë.€Žėđ3E‰ÁÎÎūQÃylĀ\éčŠ<ÅČ()ssŗ4"1F đ `¨Í>zÆ9,LĘn*:#o 6ÆĪæĻĸGĪ”I†uˆfé_žˆr†Žčįƒ9ÕūÃēRĀđGoršœÅbEGGÛũ´°°P.—ĮÄÄPņB!„Ō™8ˆũeČüä… †`g4e](U˜›´<•RĀųĘÄ7Ķ™UE§ÎÔÉba(+ŠĶj›Oü”oũė&ŖÛŲOæ yLĩ֋VžSVž=ŧ • !Ē’Ũļķ`N‰ĘpRY@˙)ŗ'TozũŨLĪėÕ˙ār×Y‹ī-•ąü›7W¤)¤ãWŽJĸJũ›6biúÎ=?į]S›Ž@âŲí/ãgŽņ¯ŨöŸ˙féēÎüŋ7ú‰˙¨%1VėYĩæ¸Rōô˛7ŸŌ†š[7.^˛WŅąŲö5 ‡ūųQ!<“Wŋ=ÔåAlž‹īÍû¨ö÷oß´4–o[ŧâ„ę9Ö6^>ēsīҜ… GėĶ3bø”)ƒdiŗ×lļ€šV~æ /:i¤æĒûō,ļĪŲ뛪 Uōų܈‘ÚĻ9ˆÂ†Œ tn›]3Đ~˜¤™ĒÂŧ577777ۍÜŦu:ŨŊͧæĐįGũ'‰~ôi%čĶ]\Yw'*BXŖņč{īîŦ€ 8æÉ0™ąē¸0'7§D;nIŲ™(Ō×ũwīu‚ĸ†‡xšjJ¯æåå]Ķņ§’é4œ‚‡?™pÃhRdŸČUĶ=fHw¸2'Ų“ÉS" íųH÷s>s}ļĮ„ût5ųįōOŊhķļfīįĮLa, IDATû/đĩNÃ0 įÖ¯NÍWčŦ'&ē˜<;y€”ĢÍywÁ†HCC9%5:Žg˙ņŖ}JĨeW¨8>ƒįūcz˜ ŒŠs{7wĸPažĄONŸLM_ōđēMŊĮŒž2¤õ„†ĄhĶĢ3u>Éī.âĸ8úæ’]5â„%+§8xsÉŽŽOh€Ž°PaâxöŸųĘėR@[|4uWZ~‰B|ú'Ī=€ÎŽĒb´åį*‚cžŒņuÜ¨Đ"ëŊw3u*žxão_ˆ/Y>$ûŖŽ–¨L@›c‰ą|ے•'UâđÁŨĢŗsuOK2ÛĨZ9]Z~45õPv… €¸{ŒõØeTœKũbך•I čZt´mîw#ꮟŋ@5ô‰¨ŽF•RųG˙ÍŌ¸žų?ķ7‹ŊžlŠčęūÔŊ§ŠÕIPėøãûˆaŧēņßëōL‚  ŅõĢĩ:ˆzŸ3khWõņUoī­åxuÕ]Ŋφ kTōߒûˆ]éņģ~ūßu5Qˇ¨ņ3'÷“pĘķģ6ī=MmHģmēeËģĶĮŒãęęzwQĐ=a|w˜_}:ˇĀä>zŠõžĄpįΝšĻîsú‡KĒļ-YyR%č.S䖨 îž0>™‡2 :qčäs‡Ë¸@cūŪÍ;OÔčŽ4xČäYS"nBŠŖ˙÷Q!SūGķūĪÉ+—úXąū\­ué1zæĖ!ž-Į]LĶåm+SN*Ą3—ž#ÕßZUGosØŊķ~k,?¸-[xŽ[ž4ÉÖז\~.ÛČa˜Gß\˛ĢF<8L›]Ũũ˙}Ĩ{ųŅÔÔŲ:[;xfōˇzī’å‡Ō'—//3ž{wÁÆNø‚˙Îõɡ6ƒƒQ\¨0q¤ũ§/˜#Ŋßî<‘ĢZ~xŸļŲĖ•EÄ:ƒíØ'ĖņĖąÃebG6—+æŪr“smNžŌĀ3ž9|ÛŲ?:6$06ļéœüā^ ›mWÚ+6–‚ˇ?0xģŨîøëī– |r˙Š]'+v}q"YĐæœŒ,tHp€”ŖČūîģėĖ›ÃÃD´{MŌ!CúįĘŽÉŪĩK›Ķ_w(ģâäÎˤˇ†7Ļžģ1S<îŗ‚šŠ}ˇ>5`õė`†I:™šÔ%sRŌ'—/ũĢß5Šā“0Zšũ]fEvꁄˆ™.™­ŪUApÂäūRUav~‰Ö8@Jĩ€ĀɡŋJ*tš›W.Ø gpĐŅã†Ožœ˛ĢĐ$<+9B*“qbŸūŖcēËē´ißm>ą|ŧ­…ĻĘÍ,‘ēxúøôLš|ųÃ6ŠT'RVī*‘ÆĖ\0XZ}h}jæÆõ>˙ >š˛1SihBB˜1{īÉm†ßFĐĩ_W\ģŽËK]ķīT<‚úÄ&4~|ĐēŊWM’A3&÷‘xz¨ŋ˙éŅëđˆ?žkÍņŊ§o\Įų÷ëOKlMAŸ¤ä¨ĢSO˙˛wķņ^‹ûYNš]cŸÔ/k×ŪŧŦÍģūōŗ<Īoxī5ITōŧA’šŖwemŲØÕ˙Õ ãëļd)!é5thˆņüūĶסmęåååååÕvĘųķįŸYé´Üî ÜCi…%iیũ‡ŽÁ´Â‚Ŋ{ ‡Ė VėMųčPOœ7ús ŋ[˙]ÚƒW/ˆh9g/Ž˜ū䉔C5đyrÎø`ąLŒFĪā˜ņIŨĨUî]™ŲŠ›ƒÃ—ļÜĨÁ¨ČüâÓ x&ücnŒŠŨZ—FØ?ėÜÅVU’¯ā9d€ŒklT¨tF#Ā •:Ö!kēÂĖBŠ‹§¯L :÷EĘŽ“8|ܔ0Չi™S ]9ķŌū“úįîJÍĖŪüEXčŋbîˇ?Û5≨ļ‘Û-xČČāßōˆ“Øōš×ISnЧ^}‡Œî{Û$Ō¨Ņ#éĮėw Ūîã\Ō]öŧ1ĀņõÂāÜ5'KžÛÉi™Îˆģ†ú䤝>T­ĐĒ” m40 ך xÔäqÁ*INööAQ㞠P!'ûģĸĄŠėdŽ @áw­nŠõåeLęt ĢÛÔ A÷čëpII¨#‡ibZ+Gk‚Ö—âđqãžô5zæf~ZĸģĄeĒO”œĐ›á Nwõ˜GąŠIūšXđÃ's K”:]Ma掔üę×WLö—p“ ¸Gh¨ ch0į@fZļĸQĨ2ĐŪhb‰5+ĪI˗—€ą¸mĒÆĖ%™›S2msĢ.ŧQmĖQ‚ČæL@šâčÉ%†jäŊ°X,íŪģ~ų5ūĄC™yW¯Õ7ëj¯fí]wŠú•Ĩã|Ũ8€É10 gO1T§Ī^8ŊĮM܋kėRrūÜÚŧ Ŗ†Xôī&či9{úĢĸÚŧęĻ ˆüú‡‡u…kizŪ‰úŌK7n4žē@™•ē.ĢåÔڕՆ JĀ1âŲŋŽõĮuEúékõ c鸔ŦgŸ}öë¯ŋn;ĨĒĒj˘1bąøWĘā6-CëŋÖqËžƒĮM..,Ī,”ëdƒ'â”_rĸ0פŌ˜§ĪÕ¨HÛ°:­ĨĘåß0„ûÚB)ŽÄŋ‡‡j8⥥Ą\ŽĄŨqčtÚ UŖNÚZƒmFŠë7+Mđ÷ÉŨ9 ĶpŲ^u6„Û=ėÚ¯ĪĻļY7Cqę›˙Í5Ų~OV,kļƒßX‘ėË´9)& ûägžėībôŋ‘ķÖae~f•!ĄĨ“¤Í[_s|ĸŖÃƒš>G3ˇ×”įTĸÅtŌ–‚ˇŧĩütû\döĮrŠå{U˙ûßīk8ūq“Į…Īlųļ@gŨ;Á´î§l÷æ^ËiŠü0€dØßį n‰×¸b $I' Ūlį ŖGMjŨ‰Ŗõ˛në€å[m֗NëŲŖ­Sû[F.UįW‹ĸ'ũ-z`Tän_ķé•Ǥĸ‰ņą~n`†6˙‹•/˜$}ĮŽJ”Ö|÷åáš[ë\Ûã˜5•ĩęqz<÷ú3ŨmG(Ž@lģ ŗÖXƒļB@ í.hsuÖ/w\UInv…Ī í.8ŦTÉŋKë1XpĨPŲžFîc#Ē~ųūŖ}_{‡õđsįÖŠ¸‡ˆŖ#hŽ=wîÂ@ŋĀāūŪØW™˙ũî*īڌėfĀŊw˜aŦ› ūâš,•:÷Į2€íåČÔ€Ž2į\VQŲą3jĀ-,ÜU$čī}•E?~zXoŽē,?ˇŌ{úŦž~ŽĮęÕ9?žŒÕ?–ų3Ī<ŗ}ûvcƌqvvžãÚÛoĸãų[zLÛ@åfÕa†#‰ —>Ļ”īúA’.CuĄü‚2úÕWŖoŪk”áˆ9€ŠüLf|<ÅÕ*IWOĄPĄm?#Ī/ü]ŧũí¯.elØšė™`ģÕųÕÁvģwŧqüG 9ŗõRÍ÷oŋU80⎓=ž]ôWŅŽĪĘŋß! đėû”힤ŗođ6qā…¯Î֜ŨũC׊Хchs” {ų5ėØqøÂ‘—gŋ‰ŽT U1€¨GÜĀ.œ=vGäßoâØIÃ< ~lßÜŽ|˙ÕĪKįO!˙ōȕ#ŸméÚׇcįhc§Ôĩ]ĒEcįŋ†ģ_8ļû ŽÄ?:N č1õ¯ņНŌ/}˙UE×N7h´mîZ‡.įîŅ‘gōķŗ+ķp„žáO|:Ž‹ˆM Íß[PüĶļb÷ø×æ|áÕÎŊg˛÷í8nąOONęÂļ­]eęĸ´=ŲĮ- ~|r_ĄEim˙Öæü¸ŗžŽ^á#““ŧØÄžôwfīžcųûŠpÜ|#ōģOŽUĻžēüĶļë^ļVĘcÕķf5uęTN'îfÕī˛į ˇģCNŦÍʼn _æėøūLÁ‘$=â<ÚĩA9ū#žęQ¸īŠ|ĮgšūSß~ib\Á×—vZá*ãĻv#‰|nZÆ˛¯¯d|ųmäžf§:snsØŊ›ā  |é?ĸwĻāŌŲc—Āˆ<{ôˆķwd]‡uEž8_õõ×?^ø~ëpDūŸ}vš?‡AŸIņūŸĨ—^øūûšë Ü\EÁ[3Ôxö}ęŲŠ=ô#ķXb1 #—ËûôéĶvjzzēõynvĶ\Ŋz•a˜øøøĶ+**|||¨L ĄzAU1rOņWîaxīŒÅ[ßūŧĀäû랗ÃÚÜ'­>#eõu¨ŧ1Ū›Æ)¯Ŧ~Š÷NK%‘Hh‡|„Ųīy -((8wîÜ­'ZØlļ‹‹Khhč]õŧB¨^BUŒüĄņ–+mī@×#RMė¤ĢĖĐÅ´¤…­į-,,ŦíTƒÁĐÔÔd2Ųŋ/.›Í …<^Įg9TVVz{{S™Bõ‚Ēbäž6ĸH$ĸrčÔÔjõãVΟĘĘJęy{´ŲīyãršwĀ@=o„Ü%Ē„P#ŋîņ욌j"­2!.xŖ˜úq'„ĒĄHh#Ō*“‡Šmؤ››•!„B! ›|´ŲzŪ\=ē˙öŧjKH>„—0҃ũž…šôĀ‘”Ë&xũåäŽNŋš‡ąōüŌí•:ž÷‚9ũü9ŋ{I¨.žXzHˇ>+^č&ūļ„6ãË´õč6ęÉ×zq;iøŗËB!„ßŧũrņ<ہm÷ĶÂÂÂ_.žéũp­ [(›ū|öƒÍXāëĸ×sܝ¸´ŋßžlߒ%)Įkā5ū‹]sƒø÷—‹&gwēačč¨.4Y›šŌgũŽy~xŅUϧĘ&=íĮ ŋôÎØÅ†5ß,ës?'™”é{2¤‰cú(÷Ī™ļcôŽmŖØ#OõE÷”G</ģõŖ§R^˙0­PQ`Âĸå âíĪŗl͜ŲZßķú¯N[…6{·!ß.\sđšQ8t͞ĩá<ĘļŋōĒ|îŽĩáúœ›Ąl˙ī|~AÞq Ī$Ęö-\ĩI^ŽŅ!í5fŅōyņCۚsŽTß\hˇ¤/ˇ.ę{É„éKįŦ/¨ĒWö~ķ§ąBģ•$}á´Íãw|e˙Ķļå}ëęĐČ7.}ë@‚ˇzĨ"::ša†aX,V‡ŋ=zô8}úô]eoëÎ℄IUWkŽë$Ŋždgģ”UgâˆŊ§OééĒĢųÛN”_Ē7/ī§“úDJØĐXŊīXáŲR•Ú đûÄ x>ŦMūMåë?ĪŊdFˇÁņ¯ôĩ”)ŽA´v IzúxÖV^ǎpÄî&ö“°sMnîĻ“•×õ¸9ŖžIÁ>ÆÖ§f)=ūŨüĘëzHü{ÍäÁQЏTĒįĀ˜+ŽĨ­>¯įxŽtĒūšH4eċžÆ‚c˙Ûy^Ą„ƒÄÍAwģĸ05îūüäq-ēzâĩ^Ž->ŊŪ|!HÚTūŪ†Ükpž0{Xŧ‹ą"7ī›ĶÕ×´€ĶĩgČô¤n> IqäH~ēĩØüŽūÁĪGÖž{HõyKß˃{ø;3|;v š´Næí˯ĢÕlŽÄC6alD_ļmŖõtQ]UԚ$ūŊf đá&üXŪū|…ŌZÚQ}§GI`.=ręķËĩŪ€ã$î;¨ĪôpˇvĄŦI“žšž§˙đå}Åöķąm~ŊüËK˛ęŗ˙××ņ–‚ēu‹‡›Ķwg˙\ĨĶ™8ˆÜÜãGDŒđåļv–F vWĘ+ŽōƒßLnēŨĒÁNÁzV[~Į2üƒŽm7ŠĒų~" ˇn*ôIÚÜPŅoÉO™ĩũMīĨ.<@õé;ƒ3MéÔ=ŖĮXƒ7~Čĸƒ{ī{kT˙æÛāAÖāMōô†ÃO?ĐU}ûŲiŲ„xYĮ&‚2mÉڂĄk÷|ˆË;^žŗ6­÷Ē{Í;ŋį7~že­O¯œņq ŒCŪÍ=§lŨŦŦāå{ž pãÔ;¯oČ(Ŧ1Äü큎Sj’wmMčCŪú¯|upyŸ'=˙“•!2>Ē÷/œũÎÖ¤˜ųA}^ßqęõ–FųáWŸ=áÅCå{ÉĄŧ\†ž~ŋ)ŒëŋâÛxåū9Ķüö•´ˇ:†Ü”%é~+ˇžÉĢŪŊxö’Û&Čn›W6zákB$|MÁ{s_]r8jÛh‰Ún†˛M˗îČ-SČBÛåāõÁ7ĢŨ9ôļģoܲBnqķÖĖ‹” ĄĖX<÷”K kÃī*áŒ6â#°Q}páōMōâjūøļ Ÿ°āƒe‘2žžčÛ9¯,ŨÜßîšEeúšĨ{šx-f¸ŧuéfĖÛĩ7Ž‹2cáÜĨŸ…oy-g/̞Η¤”Ž˙äāh?~uÚÂųKSˇü;„€°čû íbæ‡ąÄšĪāM§Ķ †_ ƚ›īe.ĻK%ē ‰€SĨS¤ņģų=Ø ĩĒĘmĮ|úŽķ@eÎģßW¨áĐ5ĐÛS[{žĒbsĒQ0{@(jŋNÍÎŌNŽ}|ųēǚK—5ÆÖāÍŦÉø!÷’‚Āŋŧ%ä6kėÔũËu"ą‡ĒĄVUˇķ`eßd_NynĘĪ•:@äîîĪ×^Ēīxvíü/Ũ$΂Ē&eé/_œt_>ĖÎ =SUŅ~ĀzÅ`MVæ†ķM€ƒ‡TǝWŪ串‘Ũ9ĮķMĩŋ4{áĘkĖPyĩ)H\[}€Xâ…âW$ Î~Ÿšŋ /īĄ^˛áRU­ĘŲ7Ū_ņsŠ |il˜Xâ.žĨcĐ\s:ķ‹ķ:8Icû‰Ņ¤ēRǏŌdîkhĒQņû†yrķkŽ—|qRē|_ūũ‰ÍĨ‘ß„ \8Y–wōėfqÂÜ^l­Ę(pw‘đĄRœ/U˙9[ä•0ŅŖuFƊĶįöÔN>sGųŠŅ|›|lߎ=˙K-ėíXöļx8ˇĻÉĄĢ¯ˇ§ØAUUWWŗ˙û˙9z´ė­Y'ËāæOî­Ģæbŋ`cîX†ŋÔĻĖKOũü›´Rߗ6,÷ã]JYr\YÅ]:ų´ßĖåə 7ŪúIĸ0dŊ2u͈Ÿ<-ĖY#ûiw–+ąjöĪ„5ÎëģčĢØęís^–ˇ/Ī6;Ėę7ût\NĄáōÆWį(ĶāÉâg­x3Qh.īIyû›ŦJø’ˆß\ƒAS­0  'rãņ•<EŲē5J…AØ;\ŌŽL 9o­ą<Ą Pi7šA™ųáŌųš ŅĶžŸÔåT3åūWfnĪÚ<ëë#Īíųp@k?Gę¤ųķļŽˆ¨Ū>įeųs[ÖļíÉąŗ:†ÂÃ9ŧAĢ{ ČĮ­û&ᯄÛu¤€ī—hëp Š ä¯ŅÂRģ9đüž_ĩå™ÜĨc×˙ÖunŨ7l9ĪÍØ3jËži~ē„Ûv!ĩFŠá õäŨ>aÛ>Ž o!"Ø TÔ+øŌÚÅíûŠ:ԅøIÁ5iįĢ5ŠzŪЅī˙{€6âcēeIk7$\^?ãõ6ĶŧcŦ’|߈`aĒĸÉtj —w,]‡—VŽO}å‚- ĖĖ6ô{%Ē IÔ3} ˙ī\5ũėdĨš*¯–MäĮ ‹›ŲŨ’ô2}HĐŊõo:h¯Ģw/~yŨ%H"æ-^61§/Z7yqYL_ÍņãąlzũĻ]åeĩBöŌŽÉūƌ÷V}vŧÆ`0 [âĸĩŗ"xw:^“Į;x3zŊūW’Æ{™‹`ÂÔ!ņ’–Ž—Á1¯E ōcoĨ7™”¤™•j€ãßīãdNĻzÁį§Nik~.möGa–ā{Îųë€PGƊJŗl]d§Ī¯Ä~s“ēŪîjĨnŖßėæjŦW™ŧUYÕ:€ãū¯ŠžbSõ‡d_í°¨ÉÃâ]Ė5ĮŌWž×)Kęö‚7x…ž1Ę[Âgs9š}šx ˛4Jhģæ  -ĪY¤Ūd+f—'&öëÛSĘɯŅÕU+›¤Yĩ€ZÕŲĒæŽå &@äī-15îËRœ~O„ôå=TįwVęŽ^ģŪÜũz#€Í÷t—úô ™ę p¸ž=~.UÁÉ{ä°nb@[žķaģ9† ęôĀá$ŽAaŨ'$q6lqˇ`Ĩ~ņ.P¸{ëd“˛¤NŅĪá§R Ö×ŲÔTˇúœîRnƒļ—,tâpĨâjĨFåeyĶUŗūēʂ–āMWšŋ>ŋ ?=ąOG ąō6ųØļ’Į€˜š‘Î6ÛÉ|}Ķ–ÂÛsũG |ēÉŪį8>ûÂEe]ŠRĢæë¯ÖÕéôšš&ô°5ųąccFzq8lŽęė¯Ú`¤Û-Ø'úŎ/Ã?^Ypø›M›ĶĢĨƒ&Í\šcŒ!  ĘøxĐÛ%0deŪæ˛tÄĸOcePî›>k]æč÷ã i — ^ĩãÃ!”é垸…“v/ŅŦܸ '0äloiāl_žR9á‹c‰2TüÛÜ7ļly$CįŦxڏ§>õƘõiĪlã}ëIë…+ePįž3yígŖ7.čé›ŧaë‚.B@“ķÖŦwvŒÚ1O”Uŧ„EëøK„"یaŗTī_üōžž %†Ë—n–,Øļ5T|ÍėˇÄmmo]뉔io}X¸rĮZPŊī¯ķSN‡/¨ú U˙ރąBĒķŠxâËúf}>zËWņBúĸļém+XšgÆØo4Ŧ˙bÄˇfڒŊe‰ŗüøˇŽËŦEŖĶß ū`Ë32šôۗŪķ˛;­‚0bÁ{ŧ%<@yxņŒwŌļNX67uōéyß[ĩŨ%j”žĖË %<ƒ˛ĘĀ4mõŋ/Ėxũƒˆ|ëRÜĘãÚ4.•‡×=÷É a‡=įĒöüÛĸ•[n;PVŲa‡ąģœ2˙ņËv͒‰}ŅgĶ–ŋĖ?7eū7ŧeë ’@]”‘Įƒ÷č NĪ~vüÁ~deÔĘU­+e8Ŋ|Âü͚n#Vlh?úŗtĪēLߗžäüā IDATˇIÎ z~펉eģŋIyaŖ!8>ųÅŠ =ŋ&Š_ŌĖĀÔÍéÕņŖeúĸoˇ7Å­´ĶHí°:E†(´Ž,O(D}™¸sœŠLۑ+úœ 0Ü[MYķĮ'ō ô0iÁÂäČ;—F›}$‘S_ļŲ†‚˙7ëå=5’Ą ß-û•„íHC^Z;+H„ęŨ¯ĖūéQŸ&JnųiZöiŦD_´nėÜ}1|˛-D¨>õƘi3ÜōĢB‘6" ÎM=Žˆežģ#oœJYr:nåÚŅáԖ•ÕjxŌ–5Jxš"žÍYۛYņdÁÂę§ËGûņĄQ VÚÎíŠß;*BYdüKËæŪæˇôA¸ĩ‡ž˛e´LČ*Ėžž>+qm€ĻÚ0`Á†šARĄPÄKx€2cņüΤ ĮøŖz÷ōĪ ĪŊŋo€†Ģëæ.]ŋeÁŨ¯Éãŧ=xl\mnÎļ+͌0Ē›,ÄîB'~Wg@ •ʤnÖ€ØÃĮ6¸ŽëãÍm‰=ôį/ë€ī$rŧÌÎ|`‚0ęŦķōrûՖ:[äËĮyôZÉ^Šņ…ž.Ž\&}­Įßį–~¤&ÕĩúĻ–7ĩ \/ߨš¤R\*Ņ–šBk”æÖ]mŌœ0!ŠZ-Ķų=?ŸŋŲĸĶk9âø(éųt…ŽĸdgE 肱‚;Ė‘Ũ7Ļ{ˇōĸkǚŸŽų€“įĖ"Û_(p@¯UĢ,jPlūâ§6íIŊ ÚŗßÚV¤N|tŧ… jķk€Í•8ŗ›TˇÉ§eŽbŠ3uuMĩ­ĨÚu­Ŋ-ŪTũujv– G$fß20ÕÁĶ](vļvÜŲ_5Ioŋ`ɟSīô—Ū;˙ oIJˇ&øßëUH?WĻOlߧʓDJđŨ‚¤ÎeÁB<™LdŦV˙îŋŠ´;ßFÔĨž˛ęęˆåˇ„LÕû–|ixiMrO*ī'ĢĐyĢ^Jųā¤‘§ŸŋQÃī+ °:ã0čĢ3Ū™˙öë;žj \īûˇôvĮœ[ 9TķKjĘÚŦÁP­qSęĀ“EM‹¸Ų0”m_•R5ūũ7C„PføĨZąņé@¯ŋŌɝ×„‚ˇ?Wäė€:‹JŠÕBčdŌ_oą˜#ķTŠšænbGƚZŗ§-ęr‰ī…“—ę~Ų”ëũZø¯^ŠÄi}"[ā ÔÁ6¯Û&0˙öÎ>ĒŠ+Ũ˙ßB8G!AMĸ„T13Pu Ĩé˜2ŧĢÄNkÅҊö‚v{\ģ : NZ…Ö UA[pĻÂŦA´6Ž…ŸáB†¨IPĀs@úû#Ay Š/ííčūüáÂäėŊŸ—ŊĪŲĪ~öŲ15ļ€Ë0WZî/<¸Ķij邸WTä<î÷Šãú\<<ĀUM-GË,íp ø¨}Äå/kŽŪ/rœøÎ@Æžė?ÛŊ{_§ƒ§ĢÃË˙ÃqmÚÆ›ZmãąK7Ûĩu%Ļą3ģĨí Åļ;cWFŊÖhÔ6Ū8UŠûąíÚŋŽ´I'ôÖô›Ļ|×ЎĄ3^ķ›d ­īt€Ëš^{¸Ž†/Z$uiS~üPīŊĻŧ ~ō뚂ÆũE?ž |ŪՅkˇ;ģ$‡y­íĩ•Ąv<ÎiŦ*oíũĀÛ?Ļ|ü˙ęÕ¯îŠæĘȰ÷.îøEû<íģúķD¯Of._yQõI˙Ls­Ņr.}Õüt€5é-ëÄG§&÷æ˛5ŨktËíÎyö¯Šį9{cęė Ī]˛2ë›l”õzZ0ĮŸ›Peę\ŸäŊÔŽ‘™Ēôå;ôī$mMōå3î§ĩhv¯ĪåŽū|އUNÖeüęä^[9zī(!<Ãü~įÍņ…1čŦ;›öĘŦ/OŸjhˇW%C\%^Si€ŅĨí-û´đLęįG˙zėfÛŨé¯×Ø?Ŋ2@ũˇ•§o 6P=AĀ:ëĘŋ(ųë§}öLhWÉĘ˙÷öJpđ p{ĀV:ŽëK€Úc§÷Ŋ°˙˜žũ~Wya‚ sKFŽyÁÅų…‰Cq§Ũ pƌyžp\åRWõ'5‡ËëOŸ˙ūØˇį>;ÖØrģųĐŪŖ‰_×T6Yēë:ÚÅҚQDsŨūĸ ‡Īßę~tTŠ˙į‹ķęÚ“-]æāÆŋûVW{ÉąĘŦüoŋĐ­é0ņĖŅ@û‰cWŽ¯?]Q{øhevšÅ ŽN`9uōŌáĸĘŖÍũž7ų,ĪQ@§Vs¨ļÔ3÷Øõx'ĮZt˙úöŌÁ^DäÖOĩ ۉŲđgƒŋ8wcō¯ãg ÎgūyŪÛqģË }¯áēSϚf€šîœnāˇPyžÁŪ×ōX÷Ķ[jKĢL ø´EßÔ§üL~mnŠô%ŲUü™ã•ydmožÚâœˇYRžÅØJyËψ)ĀÔPĶl¯ŒūpBĒ>|ct÷Ö ŅLęÄîŧj 0úsę*ĶāÅķ öž–õé)kSõŠLšSĩ”gr~ôÆAtcƒͅšÎôŽ°Ģ ÅĨ,VË?žõXsŖ…į<‘OϚF‹ĩFšâBo2ÛQs–÷ĩÃÅ ,ĀVĢ돉ūüÁu*ÖbÍK›Ęŗ‹ŲŠ!4`8“—U\Ë Î=d N|–œØO]ņ ’kÒzGnúãYŽëÚwuŪÁC‡÷8ŧ÷Āį+ŧ¸˛÷'ÍCčOU–˜Ęķ/P/ŠŦĘæ›*v‰Ų˙=™ @wĻÛbú’ėŗŦˇ-ĸ{Ŧ{é`lŅ3ü€@_>Ā6ՙėšÚ Ū’p6 1ž{3'|˜ÛåôŊįĖĀĒJJM ēƒ_ƒNcüVŋîôåˇUuēáĀw÷P˚0~Ņ‚…ūücU§´FM @ģøMę„ģņ‘Ŗ`ÂE…ē ųæÁĸFß×u؄ë¸ß-m:ģŋōĻųF‹“ûđĄm7ÛáĀŋ—3sā´ŪŦlfā8ÔW6eҏ!čŋM°OäųꋊÖʂĢ7Ë/uøJ†rn´vŪ§õ1cFáĘu`Ô8+0T2’w˛Á H& wGˇ€Ā÷éK‡ĘõĩZ]=ÚÅkÂ(×!Γ$Ž?j¯jāĀé6įŋ—\€1æxž>T×^{ŠáĮÖQ¯NÖÛn^#Ũ*n”_0ā8M˜ķ›ģûNŅyËXŲĖØNƒ7ĀKĘßsžÕüĢÚXUŨ`¨ëp?ŽĶ¨‘/ļ|zÆX{ŠąŨS,qžŠiëk§QŪoMēēũSY¤ X:Õ~=ƒqĪ;įO˜ü§ÎĒk)?ßá;AĀĮÕŪÂúĢfß°8=†?7´G@øš€pļĄÔôš‹Œ_´ÄkeüÛsšĪMtŋ Vb\mBԜWXŠ?pÅV™ođ"˙à ķÂxc‚ˇ%w_įž1ē>éŨWv˛ Dō†{ƒYÉ3•$,)1[XzLđ:Õ\1ļØŧ$ėPāŽgã‡ĘU[—%oyka:€[ĐFÕl1`h-˙tÍö5–×[ž:u‰5Ma)Ũ•e”ØãåJj¯8Ę7rג)âG]7ŋ÷æŽË,¸Ū!̎ųR%])VŊ;ûŸ\ŪnĪĶēËfKvZfž1ŪN”hOúÅč ˛†™˙˛dĢV–ËŸî<,HŠė3je•]ŗP;ŪŪԘšģR—ųØ­­M˙ ¸ąÁØ|nųÂĸ‰Ëˇ&MįÃRūiŌú5° ŧeËvŦOØ`ŋo˜*fz-HéË`ŽĘÛ˛ã#˂ō\¸uuÖûāNÅj÷ž7{ˇÉĖōũæ}˜8™ °MęŦëĸÃBķŖ&Ŋd N|œh]Č[™SŖ×ņū›gŊ%nUŠôÅ{7]Ŗļ¯ ÛČ?újÍx4}ü9ŧį0đ)Ÿ…æ'}đæ[Žßŧ ë<) Á~UįˇŧžĻÄėÂõöHŨ1ׇš/ä­IęļØŧ­_¤Å%xëcdšŽŗ~ųÂ"ŪŽ€k˛ķÜ`/ŧŖ¤‰å¯Yx5"x[jtxŌ]Ō–ŲoXđÆEĮ‘˜…`㚟~úŠĸĸâ7>S{~zôŸ_ܧXyyųĖ×ūÔįÛ×>ę7ŋv;ķkZhډs§åúÚæ.Œ—đg/ˇgĮįū9Qígá?c\ S—ž ‰;Ŗ}ČÆz¯“'1Ä ÅīžU~#n^ žyȸ Č#''•1ŠHæíéÆ~æÍÅu„÷xiįûoo9:8ēpÉ:8@ @ ü_oEQyÅ@ @ ~-8@  ہ@ @‚7@ @ Á@ @ ~%Ø,yŽãæŠîIÕC ~Ē‚RUˇĸQ Ĩî4t9öŠÃP­…Dø8‚ CSōC ŗ _I{ę°›ã‹Į&dH)]ūʈø|iN„xĀh÷0Õūd?!mÖ¤DDÅæ(…fģ50ÚŦ¸ØÜŠzŖûÄ^5pS¤ÉčĘjˇoôSH(Éˆ•Šy0¨ã¨R.†¤HUđ†8ņ)p"tąq™5Mtxá!ą{’¤bšŠÎ‰Z—%+ˆ‘<švÚ´˜ÍõYG”Zw$vq\Šw^ŧĒ3ã˛›§– ęØq™ŌŧXÚ^ö?$3¯û™ˇööv–e†čßÛˇo?L+ē›´ī‘‡c—ĄîŌߎŨę?ŌžÅĶ&ˆĻųŒô°ĻĩčaîC0lœxÚŅ´ ĸI۝sėpÜnŋ Žī„ŅÁ“Ež.Mu?|”÷cë}ZžÖXaĄŨ]Ļåx^åY pûzÆgg ëÚÜ'{kúHį[ÆÂũ§ŋšyOõ“ßÔWĩv FŠ W,."WgāV}mÆYûK7ž+Mø§ŽŽ# ėÖÕpI“Xh`6 ĶÁ1mōØā áöNJségÛî|?ÖŦļ´á''ßLs'7qčK^ Iw€ČÜ(<ۂ!:ë*Îēzį>‚6žrōcĨ?ūėÜņ۝ŸŊėãÚa4,ųŧGYÃĨ­Ã]…@›ņęĮģĪYMw§åxaM}įĀĻ{Pĩ„ûFmš‚”åėř3Āh6Į›ōãæ*ĸrjJcä‘GŦ‹)‘GĻ"^.ŠŠˆˆP„*ĸ˛l æŠŦĨ\.—ËC#TGū_Ž*ˇžéPÔėPEDŠĻĨ">ÔļĖ莨"CåĄĄōĐÕ0Õ) yDlLdD„2442ĨÂÜoŊ9E!ˆ‰ęuŖIRĘeÖö"­ųĻ">TŸ!÷÷W~ömZ¤ÂŠÜßßß*šîˆ*RĄP(BC•ą9ÕĖ€ēØx*˛b• …Bǰå¤ĖšŦehhh¨\ވĘĒnŠØŧŠÂrqĶÜĐPELŠNQ(RĒ™ž *BC#“rŌâ##"”ŠPeüėéÂT§Ģ õé‹CCQYÚ{Kûũ­÷`ĖęX…­vEL–Æ ŽlJŠąœˆnj/5÷ ę ĢÅa!>4@û„(ŚÂ3ŖÍА+ĶĒæŠ$Ĩ"žWBŽŠÎJАF+%=zÎ;}Ÿ[oTĮ/PD¤T3ĘUY9ŅŪŧ>öėŅa väS*īžĻ@×Ŋ *—ËåĄĘØ|-†574O@Ņ4 b™ÜOL0 oĸTĐk6ÄT¤gęBV‡1@qÆXļyqhDlڑę'”Š3DĘcmICA¤ŧWBB›Ĩ”Įv'ot9Ąą}29vÔa´”\9‘Đâp]qÅ}DĨ%ĄĄ~BĪ;Л2ë,¨Z™–8m†āą'wû†MI…ŋ"Kk ФrИĀl1š)™;=pÁ]9J&Š‰ŠŒP*B•ąũōT}ÆBJNJŦõĻ‘Tj&N|v(V¤ääe„ģ÷|E,•Ši´DęÍcæž7ÎÁjgŽŽh‡ÍĐÄōČ@ļŦXË ŠŦ”•F #¤liYĶ@uŪŋĄG[ŗgd]~Œín*WÆZ„LuŠ"4&)>2Ôß?TUüEŒŌúĖ •ųû+ŗ´ ę”(ĨBĄ UDĨU˜ņŧ&<ۙˇŽŽ†š_ö¸ŖŖãaZǘ+57†û—o,†Úë7Đ3Šå8KÜ<÷ŨûÕ¸aoM™<Äūŋ‹ökø>q—pđ |k´#āģiųíz­ąū&3˛ķVŨĨvšouŠ ęĶWē>6ú­ąÖ?<_ÜúÖ(ęö‰É•UwšOęīøŪŽ>Ų |ŖfržwŽũ&ķZË7ĩˇ˙`[!ŖƒįÎíäÄqrÁõû+õÖŌWf ;Îī+ŪV×e¸ÚΚéXo1æ ž)i8ĶÂĻĖ‘8uˆZžËÖĩ]ŠočzųËļģUuĩEwsh‡Áxō WÛXtß6žŸ8W,âč4UĩÕßÕa˜ÄkĄí7̘bUœÔ †Œä˛“LgũÍ.Œnûf_å77­a§ƒË¸Ik^ĩÕæ>=đũi\gŽcĮ•ī>b—҉ü§cÚžâmuĖÉã[ķmîđTū~ĶD'könS—>ī¤ŋđ—Œú[­ˇntBXkßtRNm…ũjGQd„Ũ/j+ÎÎĖ*Ö gD,J;,ŗŽú­[7C.ߟŖ‚)-`!Plː‹aȏ˜ģŊL™!gŽÄÅx§¤ųņ`ШëŨåĒđüsR^ŧ 0šŨœÜ¸ÍMá9j…炍ˆ˜\ŸüpČc“ÚŦŽQ¤‡į(Åũ—œ7Ļ„ŠaŽPÍUĨ…åÅûH"3 â…W#Ąûë­ +Vyīɏ0Ģļ^¤ûƒTāIc÷ iĀp$fîæâe诨ėšęØÃũx™&#K‰mĨyŠ561´,2u]§y? IDATÅܸÍŌ4yĄęĸõs¸CPÕÚuķv×-=ÆKī˙õŌŨīÛnŪTÄ"Ūpë ŋ=ã9 î0čėˆÁmjĐųŨūŖßŨģ!1­¸ŖûŽTuäfāä2tXŋ†œ†pŨ‡ĄlĘŲmÚÁ ‡ËÅIĢėõú–†î,¤“žŨĄ ŗhØHŽŽPČā0RDŖŽŊã&Ķq/–vš÷ōąg¨3`M•dē6 T-Hđ6PäĻQÍ^\H…%Ļ•<ėJ-z‹€'‘Đffmaĩ8"ŅēŅRč'Zģ3MąÎg™\l]ų ÷IÉÔÂŊA $Ūî4Ę]Â3kģŸr÷ ¸b‰xCŧÍéđĄšŠ37WčŒ,k6šefî-Q„‡ú‰{,ĻÅĨSĢ÷GúĐЖŠktėæ(kËč âGĨ¸įÄúØ7¨Sr™đîxÎP6Pqđ$ōH•<Ō\¯Zšā•ĖEy91’Ÿi;“PžH–ž’¯ ‹ÔeķÂ÷øŲi§:bôÊcŽH‰J1GfDûĐû5ÃöœRŌ4`Ö¨"cUŪ)2ŪšęĶ7x~ĘHŋ^ ųÅœ7”ĒGmįŠē#‰ûv*Jčg]\Kå‚˞zF!¤ûۚēGWëíÍ@šģķŨΟ NüĪs"S_’’eßƒŅŽö‹IIŲšÆōÄ cĻĨ\Ģ_xzÚĢs€†ƒūFžhŅdmßTV­3ŗl“E`d  Üeá!Ō{76F›“°YžąŅƒēāb“1}eD:0HŒ,~^Hđö„šc¨mŽë0znd ß´¯ŪxüAößģwv6Č:û]+œÆú%üq” Üžp뇆;& ú[ØXĪ]Gߡ^Û×ģ­úž˙ģmihĀsœ…4Đņ˛?ŽiĶž \.ĐöØ öŖŗYč´Âķū÷"ŌNGw÷ģŲδ¯F{9Ô~“0āI€0Đ6Χ§pFü9Nņį^ßļõ ži –æ[c†sēnčNÃi§ûUÛ¯x?Ķ9_dĩ„Ou{RŊ3ŗĶŖ‡…G†+üZŦeûņĖãķ(ĮŞ, P”Ą .6ß[•–#Ķ眈Åí;ęMqę‰IYļiē™jORĪl3ØcX€ō^–Ög‰1>/OSVVQĄNHÉOĘK‘>Xe;Ë ƒŌ呭§ÍЉWËS’s¤BT§ĖīĄŧ^á. XŖ-ú3YJpw  ŦŲĖö|PkķSÔŪŅy~OâÉmGNsŠ**^—‘*á™Õ‘Š\ģN.Ë,æ-:â'æA›*6†ÆįքĢî‰D‹å!î›ŗk˰†dŒ&;ĨFē.I2ˆâŒŽĸ0++į„N˛1#<ä‘"7v€ŋûĀ“. aWf tŊ—ŠîķÖĶ]u$wžYk›Í˛f xî’(ꤨÍõŠŒ4[tJ=L ļũ¤āIäJŸÍ)Z3î?ītßJیĒÔۄuĐÍė ī 4X1Å$N| œh/4M‹‰/ķKɊŊ°ô íx>JU– ËQF@.ĻiŖ7—­ąt'Œ,ˇ;;:@øôdAQŒ&%js͞´Ô$?!S;;ÅūĨšôØ\îē=ļ›Ëõ‹Íčĩ•Ŗ÷ŽÂ3Ė/ķ;oíßV¤īûˇĒ‚&‰†÷Žpöe]Ēē€vŅV}ŧīģŋ¸Pvŗã|Ūw…FBŋoßwÛwæ@m—nÕÖûCAŅšŋ6ŪętpæčŦúŽę@ŅšŒs÷ÛĄ:|¸Ú …ŋŅv<Ž8ÃĮyIi ĩ>1ëLÆŌęē€ĄÁAjĐÅíšî1Ģ}VÃ7ą,B•Qp8I)¨H_njMëŌĪ6Öæš ;āŧ…įâŖËÉĩ­hŽ.Õ@ hsSSŸBŋAuŽZ:uļFâ7¨3!X–a­Ģ‚YÕâ0ŠĐb4Ķ>rИ ÚŖŊ2ēü„Íēđ”ģĪe÷ĨNËĩûÅč*Ԛ‡x›‰įâŖËĖ´Ŋíe¨VĢu0hÔ5”ˇ\̊•ĶZ­Í…Ųž4÷ÁŽ.˛X-˙xÖcĖZ Ī/dĸŒ5ZŗÕ!4ÅC“ŅlGÍ0]~ą–˜ęâ|O˜7€áˆJuQš'o/36Ŋû Lޤd1ĘXųŖœęҝÃčėČɍĖIx€YĢ1˛hIČDsqŽõåŗļĸÂĀK(­íM!ĻŠZw? Cé‘R댖Š.Č­HģߡĶĻ–EߝŗÚ/莤D)fĮdUûDĻÎO‹UHéÜsŊÆČsP-āA§5€ĄFĶÔo(Ņ>á‘îęMۛf,ëŋÛΞ:´$DĘĒ 5f:unĩ8L*´NŋōĶrúĘTįÄ,NŗDîIŧģũs ėéQ]jĀ\]]͓ú nk€žaÖäguŸ jÖQWŒļ8Ģ~2Áā:kļž›d(Í*dĨ Js˛ y.nOˆŸ%'öģ1ę â#T5ŠÔ=Ŋ"7Ũ‘Ŧ,ë9ŧĨí5Ȋ´øL(ŖĨ<Ā=PFUä—Ęō+(Y ûuØĐãŅĪČæ&Fč'˜ĻjŖ]SލâKeIIŨē ũÂĚ´LëkmŒAŖ.}’gūÃų…2o7ßĐ\iß˛‰C€§YtŪŽˇmöcĒęnXīˆBųoÄÛŪĮĻúMÖéŸ×„ųœōōž‹'úãįĶŧ\aly~õWô†;pvŊxū„ą€ãžlšŋ°¨æd­ņ;#‡anŖŸ)ˆuɇ\ÜWĸ¯ĒĶÕ šžSF"œ÷ĮąēÔ×Õ՟´ˆĨŖ9 õÕ06hÂËÚ '7OV´Dŋ}ųqN`âūßK§ėûGõÉõ'gÁČĐW'Íí8øâöM÷˜Õ>Û1œDĄ’E0ÚRuŋŗãiŋČhī˜•sKšO,žß~;Ą"QU“*gi‚ĀÕŠ2ŋE˛üøP9O’ŧ+´û:IxJŦ6~ą<…-–ĮĻ„K€ÁŦäOÄĪ=a63´$d]r„‹ˇ9BÉđx<0čˇĘ͔nŪ\aäjŖfg $‹Ō2"ĸĶÖmVÅ)˛Ėx>Šu‰ōÁ›I¨HN1Ē’ČU @ ¤‹R'KSâT5 ÍŖšŪáņJ h÷e‘•ō|_lF˛÷ Ģ–ØŅ…öQFzGEŊ"įyGf¤IîcŊĖwhŋeĢũââæ*yž€×íBžl™";vļũ:ŒĒŋœByŦ˛0>B™-āņx4k“\•ĸŒOš+ß܉ËRR”ëŌ–ŠT )\bš*Y) æ˛Ė¨Íqf–Ī;d]Z´5Ma.MÉ4†ôÜĖEKí@O\–-}ä“ō Qs7kXđŧÃÖ%ûŅ-^&Ž[ŦČįō„žŊ ŨâJIŠ9$Â^ēž:´lõFyBŧBn˛˜ĢāfMfJž mYŸáP“›YÖdĄ6ĪUoJžą'ÆĮn LuÚĘøbmŊŅXĨ(ô‹MM’ a)Ë\g Ę[“ëGØ`ŋo+˛Ōķ•ļTÍs7oŽ7˛,¨ąËŌÖY'ĐîTŦ63J‘n43ŋČä$)`šŠ3ĶkbÃ>ƒđU/ˆŸ'Zōbb˛jššŒX9ˇÔ;2-M)n*Î,njĸRŋbMA B2ōT~ĐϧÃ;oŪŧ>~˙ũ÷ŋũío{}ÔķwއkžEėŒ‹˙$˜ę”ˆx$Ũ{i‰@x ‡˜Ą rAaøūŒĐĮYc4ĒŲ*AÆĀ'Å<Ɂų ļe~ÔQŠÜđ‚Î"N$NüØ=!•f#R*>ŸO O1ö3o|>ŋļļÖËËËîˇĩĩĩ¤[„'6_•Ö÷HjZĸ\#ãã~Æ%ŠėĖĻ@•\øxÕ4Šk$‘ŠŋĖDü—l‹8‘8‘@ üаŸyĶëõĻšššĢĢīË`ŽŽŽÃ‡Ÿ4i’››ÛƒW „g2.2ÄĉĉDå_L*’byē0ķæīīßŲi˙E,GGG.—KlG @ ˙ĮÁEQE $@ „_ ā@ @‚7@ @ ā@ @ HđF @ „_ ļKš››ŸHuOĒái‚Œ  1q"q"Qų`Ĉ¤+>ÁÛONßH~BõOÕŊŒ  1q"q"QųāæõČīŧ=Ũm“@ @‚7@ @ ā@ @ HđF @ ŧ@ o@ đ4ÁąûéO]lõĨ˙wŗŲtįΝ>_9::1ÂgÂÔį¨'"A[mŲÚ¯pt‹‰~ņίÃ*ˇī9yŦŖäÁëĨN—ķėŌ‚?õeÕ+ÃHy0Ļ*՞:†/˙ī ņCžpŨ×ūũMâ™vŒöO™'r"Ļ&@ āíĘÅJG‡įĻM›f÷ۚšš++}'M{"8š÷“ “¸ūŠŦâ$‘ŒômÁ(>ÉL~a˜†Ã ÛO\ƒûŸ}ĩ‹~´Z,įōÕ댰!KyÔŧí~;D{<åĻĶŠs*~;WáA`ĒļŧĪn;¸ÁīQ™LęC%‚Ų~\Ļ‚å턨öÄ~5‡Š+:Ô8%\.ę˙•áÔö5ŠĮk,āy¯U­–Ûoŗ!kųŌŋgīūŸōßz<)=zNĒo^ÜļĸúîŒm‡’'S`rWžWąâ@ōdę m(ØąåĶ  ,܂VĮGËø@ÃḤŦŠF‹Ĩ‚qŗ×Ēĸå|VŗííåGõ÷„1ëķŊkũ`¯ø“Ā ^ŋ|įåĻfĶč…Ķšv‰:n~ööØ˙ļ§ŊûĢĀRą{ũĻÂËfP˙čä5ŗ$Ô}<žŗ&)OŖˇX@yËŖWY/°ķ™ėŋk×\ą­?^0{MwmÜIkŋJž%ÄCô ēīHŲ’°ģ¤ÆdaÁ÷–/Ûļj–˜TÁ_âħ‰ÖۄfįŌl¸ûDĶŽOØ{Nką°”ĮŒ…6Îņâ=–v}뇾dsrfY3ž˙e‰+‚ÅũņíIéō,ÍMˇvc˜_­Å„GۚMÆiĶĻũôĶO?ũôĶsĪ=×įß^xĄ´´t•ßųßIĢëâxüõŖœē2‹–I],G?U´ĀëĩWW¸XĒ´ÆNįĄ@Gã™˙ųōZ§ãđ/TUß4ƒ3vŌ”wfŠ\qįęˇĮˇV2!p1V^e:]fŊôÖ8g¸Ĩ?|Ŧę”ļĩ⏠ä¯ūnöo†āļ>åãŗõđōHSÅÕZÚ{ãŸŊŊå¸9´4Ū¨jFÛ\ũVŊK Ļʓ++ÁYžā^õ?_^ë¤G*&āôųĻ'ĖŅ^{i˙ŋ̚;žģX1ËOĘwĐĻĢŨ_TŖiî‚ŗË(Ļõú<˙ęĢ˙3y:-ßj .MwÚÅ/`Ō‚3ÚJ>?ūe3ø>ŖŨŽëǚģ8Ž#į(ũƒøŽ}MÛiܡĢŦœīëĄ+ŧœp[ŋs×ŲĒ;ôôđāyâŽĢį5Kõõm]įyßŗÆŽæØ Čq÷ü/gũą:ÆëOĶ}ž°ĩÕ¤Ģjé:bôŸf‰Íå—ūU×Úîč:=,`ž×ĻÆCE5§›Ú;Ą#Ķ?yļ—3ÕųFʞ:ÜÜõq!G.~i|ß^vįÚÅKËŽÖļtC]]fŊ¤ã =zęĶj‹™éĀqv$ķ[0y„€ÛÆŖ˙<÷/m{§#=ŠfžÎČęô´§lÍŪŦŲ'ĮWŒį=N}Ļō܃–‰!B ādüķĀ3p3ŗh s…Íļo´īÚĸŋ?˛7ô'æyËŦÁ_ąĢXņD5×åe–ŠæČE}§Ļã ɗg$Úã‰ęī.O>>1)ØŪôÎ#rWqdˇÖĨ‰oė)ĸÁjîõœ†ô%åŪĒCy/ōNmYŗĢ¤æøsĖ…lŋņÕŪ`!XÍΡWĻŠhJļę“D_ }AÜŌ-{gŽōō[sāԚîIyņ{oOq§ ;d¯8LÕ đņxŦ0N(˙0On*X>ŋđņ•´§{~{‚Ú#qīGRJŸŋ4ĄpĘū9ĸ×(EaqŸmķåĶ–Ë)+ŪK(ØÆ7Û­mČR­?pžÁ(ßĢ—€ˇĘzÛíũŊ-Z*âÂTŋbËöĒāäɃ*ø Cœø8úĸ8UVÅ÷zúžÂ¯ŪąA*ĸ˜ēŧå+×gû÷][|íėÔo*NŪrQöŲá9"° š+ßM(˛Gnų4aģöOŠÂĘŅXqjĢēŽ#_• Ú/՜ĒÖ¤q¸În úšę €žī:ZÛâˇÜ45_ÍÎÕņŨGŒrn­ok9u´fēdŌčN}VÎy žģ›¯s{eņØ×§ÚÃƒīš¨¯¨4iønv‚ö›$v6Ü­_ëĐiR\5c¨ß$˙NۏÆÚëãÔÖŌ1täH_>cĨļĨōØYž{°rsúëĶWģā8Ôk ˇãú§-j3iÔ9Ÿ<Žŗl—ĘƒĒڞpÂÔä´ūÍRELj˛¸Ŧ°ŊŸ„pļ|åŧŦ™ģ?QpĪÅĪ[oœėeŅ[ĖÍ÷7>LīEæķ9 Š9@‰ä+VzĘĢŋÆ._Xœ˜”ģ$ŨoĮgá"0úã[’ŌÕ×đå+>\+Ą.ũ͸sŪžT“ÉblæÎŒß;™Û{Ŋ9ũ͸sOĘhšwSĩ}A|‘@IBÖn[ dĪÅ/Ü™WŊ礆˙į­˛īūĻ6€EßÔ:~ÁO\}ņŽŌ/˜Ā˛”oDbü\Ø×Å~Ās>gSęá,ø3–¸n:ß\•“œWcÃRŪķ>Üæ›ķÁ‹ąęíY;)ī%ŸlķĖyS…mģŖ%w4›ų3æM1ĢËë[-FŒ_ŊmCũt™bÜŊĨ°šZ5'›ō˜ŸøĄ$Õļ´ßßz4û ,%q+ļ”Y@<߈ĸ?öø;k-­ë_ŸGq'­Ũ“ĀģÔÕ¸ÍÚčIđ‘Ī,ĒąY ß]|4`×Îe>”ĨbĮģ›,Ë>OöČlŋ0eõ*ĻjKwĪũ_SÛžl4!iélˇāmŠŅ>Ķ×Ī]ūnEo{öč0[7ú_î+'—­ŪũŪĒÂ%’/ųpcˆ°TÚūÁÁr š?坍‚e-z# !EņFP´‰Q€-­h1YîÄÉü^6aĪ}ŧW?S,tv‹ŗĻ˛ÔõĢ,^aķ#įËŊ„O`˜™ Vž}báĄä)ëßGJ}ņnž#gîĒËŅ{?”sčs—ŋ[ąđ‹äž™;ę°5Åį(Ų։\ĸ7ŧŌž3Ė(‘Ú#8Ėpņ ô¤N\ŗ\­Ũ(Č¤/ÂΝ}įãę|ˇoؒ9 ß>ôÚ‡į{Nļu!ŗÅdĄŧf¸Qė™ãšŗ SŧY“ąŲĪeÉņŊķT}Ƃ|Ž÷ĩã•z‹ą™š÷ŅēšÄ‰Ī¨Eŗ’wWī|{MĪÄ̓­szĖonŽą•zÉ˙ÚŲĢ`͍z3D<ŠËĨĀsĸ`Š­Đ‹æČøéŠSmW[ē r”“ŠöÃ=WŽßiģÎ@ŌTSŪŒ÷–|,Ÿ#Ē=ž˙FË)íí Û wúë˙åÎá8:Ąņ\ĘŅæN[EÃ^UN4`s÷dq36`ԕJ-\Įx*_ ŖŅZņČEō{ÁʼnC;hŋ֙ŽdjĖEΝÍC?=uĒíÚ1ííŅ?TŨhqĖŠ/ š}ú‹cû­ĄĮ-Ũŋ´]€`ÎŦ “\ēŧZol=Ķ^uūfÛ8[Œ6öĩ—cĮ išøīõGZpĢšĨs §éÜÎ>ÂOķ.5˜ëĩˇNįÂØŅ¸•_ŪpĻž:ųŋÆ8uŒjŠüR×^[˙cįp›§ŨĮŋ˙š˜O;: é8}¯-'[bsää÷#žwē~AĩˇÁÜÖbę„km­†œGŋķ§)NĮÔüŖģ´Ėérãœ?r틊1/I˙ēpĶ„ĄĶ_ž0~ĐúcV~Í5›čN’™/)nĩ˜Āač0î îŖgžĘåĀĀxå$&c­ÎŌ2 ĻÆÖÚ;Ė-] õG¯vôô?Éį‰mīŧ= 0ĻËÅŗ˛ÕzlîĸÄ2žĢ×ĘJ>–}ļ?„ļŧl€5dÁĖĩĶE0^°$Ŋ,ė#9{ŋåÍä˰ŨĢ}ÆDėÚģZČ,į6-ŲrāĩŅ"€55QÁkwŽ–đš<ęíW–ĐÄŋ{hŌę>[Ŋ{}6õūŊãy°Tl[úAaĐū0{ēØ}™ŽoJŊ’x Yč/^ĩŊtōęĻ9˟Mį‚Õkę(WßÕ&•öÅ9S×ŗŧMAŨĄˇ_?hŲąķ3ŸÕl›Ÿđ÷†%t]–Ŧ SoņŪņE¸€E=°õ"ER;eõŽCb>˜ŠãßŪĸŪŧaEΛĨŅ_ĢĻô‰T™k&–Ųb9.ŸbMM,%›ŋuŨ…ˇ×ė˜’*+ÚT”˜Ôcri*ŪYÄ[ø‰ŒÛ§įÔļU~ĀKübĀ˛Ļ>ÆŽœ"ÉžZ"âL]æ‚øL|ƒäüöUŠ ; e|˜ëJ4ÄaŽ.]úÖES_YL‰Iw•bKUsV•ZÆÎüpWīŨŸÚCéec–íķ¤€ŠS^‘É” %ųˇ˙y7ë-xg^°ĪĪ7Eņ˜ĩČ3'[­—‡‰˜ēŧÜ3(äf IDATÖ D;“Ô>ęXŒ×,”'×Ē,ÅåĸšÁ<8Î4?pž;cĄ`ކÖōUo„PāJ^œģ:.Bú`kôčĀ—Î[Æķėáöōæ%ī篟÷Q˜č>{!đ]–ŧċ}ūĘĨ›Õ!ü~ˇĻ ĶųL]úë+îødŋ/×|ęũŲģ/ząß]…8‘80ŸĪ9)ÆØKG>’vVC…Ä­U¯z/¸tü _4Yģc:ŦțĢ/,m ķ a1˛0›,SįÃŅßČ3V~&âŌ€Žp邝å!ÉA‹ž}qõŽ^.—G/`*‰_•)ˆ›->_•É.üčđ‹|°ĩé+Ö§ËŋX=˜į5áŲ Ūž.‚Š#Q{Ŗ]ĶxŖãxÆŊ¤;ŦąąĘĨĨā{F'7hŗ ]Ė×hžœøņå{™Ã[w#Oˇ‘\WhkmŠož›ûę2wē9ûiEڍīėĘpÛÜÚĀu$ׇ~ŪhCKKg;p¸õ>ˇŖŖÕŊŗ?ûW™ĶC"GC]hЉ.č'ŧėKŽ ĮZŒ§ŽŽv.Ķ'rë×ÛtV:VŲŖæ6ĀrhŽÛ°!N@Ã;ŽC]îåâœ8ÎCŗÕ’&ÆęG>€ƒ`$ m{g Ķî€ĸöĄŗũęÖëwÍÜ g¯qĶËËO5ˇ–ŸÔ”€Ëô9Aķ~Ķqú§ö×1€ÃPįîô&ĐŅÚŌ\ŋQŽOҏcĒļŧžĒˆšš!uo°äaßBĸøROp=ÆR–& ĖE5ns}šĀ/į vg—^ķzG&QP¸gú§WLហøŪ" %ōāĩ6˜ûށ‚âŠÆō€įėmÉŧh‚Ĩ?ēw{ņŊ‘eÍ͖ĀV(°7‚ũî=eŲęŨë?Ļĸ÷Í÷ĸŅPVZĢcˇ/?cÄ° ebėéŽŨåįĸ˛ÆZ­jiļuI•Yގ/wûŽõ›ŽM QX6˜MAŪ>×mŠ7%đäŗU&4kO—ÁY/Rô@,ÚâŦ-Ĩ—uÍŦÅdĄ<Ų‡ī,üøËVŧ÷fi†ËzžÂg>Ÿųņĩ m!ĸ‡ž0Õõë0Đ÷—“m>—™z¸˛Ņİl}ŗ‡™eĩęsTČGÖ×ŌxžA2Āp*'—ü|w{ápæîĸôÂYģlšGJĻ*<Ãę ’Ū]žS´˙îģ(Ļ’ígįoŗm5œ¨8xA‘k‚"-ĩųÉīŋ5'kŅgŸE{P?ĪpäËį|ŧë°6$Bˇ÷8īOėŊ'ŲGúaÅąœKY“nž÷Q´'°QÖį…ŗi °4$ŋ—ėuXĀ{œ¸Oßāú…EøõRhüēŊęuĻōMĢŪÛâöÅÆîõûûv*J0Îē¸ ’ĘøŸži`BzįT)žÔ“€á%piđöä D"^‡ŪüŗßS‰˙ķœČÔåŦLĒŠ dzílNŌü=§^ļõëy܊f}Z˜•}~JėäņŅIËļīxÖn–įæ!é°Đ“¸/č™ĶĪČã-Wrļ'—×\ŗ°ŦŪ2ÂÄ@‰æË§Ü›° šIÛ›ŪøhŖ/Ļ’Â+zãî÷믉‰˙ÁĪk ŪžÎžã\pŖõËj:á0u‚h*}åXĨî_—ēÚoÜĐû–Ôa!Cų4Đgņ‚Ycų6mîĀ… ôÍĪ8û}ę¸ŪATË`šs`7,évx.¸ŅÕbjkך“ųą\]9C;€v´Ūjé„kK;špy@;†ÎxÍo’‹5&šĶn_spî#<0ėĨÉCl×|{‰Ã#ÆNåN|g  c_öŸíŪ}c§ƒ-0¤CīČąˇy[›[:Į¸rēŒ7WÚi`QīEm61ŊÖĮzõŽ]o‡žzŨ¨m2VVū ii=Uv}–‹ņpÛ+‘męĪj§!Öļn›Z§į°OÚwõį‰^ŸĖ\žōx؜ˆų!ãZŦe؟Kęîø,Ëe*P­Ī÷\›ē3HLés—ŋĢąw­áԖ5ĨãwØĻéđã>Ięy`{n°ÍÔo#Sû,1ŽúâĢĒō˛ķįNė^ēũŸžRMy°ĘvžrƒŌ呭§=đ~BiP˛ę3)Õ;ßNčĨSŋ^áÆ§X“ā°˜XŠīNõœĩX؞ę†ü%ŪKžđ{On;rZĘ7ÅeRĢ>Ú%÷āYJ/ĖŗŅ–í=ΛČOą(:ÕÍ4+)¯fÎÚ{"Q"š\´å@­^ļsĻ×LZč1ˆâŦžĸ8'ûī%:ˇā Ûæ†#‡ĸËÄ´@ĘšW/¯Š4ĐĻ;ømÍŠķõ§ĘĢ}}öđõŽ'¨‚ĢĢĶ%ÍžŖ—Jûg _ķ€Îēŗi˙¨ĖúōôŠ6€v{U2ÄuŒøym Û÷ü;åsõūģ¯k Īí´Ÿ8våØųúĶĩ‡Vf—[V07ŸßŒĐŌŪ ;ÖØŌųčæõŖļ†´/ĪėûGŲgÚ.`čôÁũŽé§š<¸yčŸįō˙Ũhė÷}ËĨ3ëw•}yáú­ļüo¤ŗ3ĀręäĨÃE•G›īJ žę  õĐ×gōVîģĐū”Œ=e-bĒ>UĸƒIsĒ–ō RÎŪ¸"ˆnl°€ĸš0יŌvuĄ¸”ÅjųĮŗkn´đ|ƒ'ō)ĀTĶhąÖHS\čMf;jÎōžv¸¸Øjõaį,o.l/ßŋöÉW̏™›25Ũˇ ƒ:=ģcöęG:œą_‡Ņۑ“5á!÷÷⿯ËF%‘7ĢķJ-`n8WaâŠĮPZõ9°Muz¸P0œ9^Ú`ļļđP­`r÷QrúĸíÅüw–ܝŗÚ/č‹wž7{áûŲu^‹ä%E+&?Ō)–†‹Í\1Ÿ(ēF˜j/^ë7”(¯đyĸÉéM˛ČĀ~3I{ęPųļ´Hc W˙ŊV2Eh~fæž2õË0ä­\•iž÷Ięüģ§ę TƒŊin]šMKmáÁZŪ¤ņ‚Û oX4…9ųUÛßę’j €i8ž}ũųƒëTŦŚ—6•gŗSCÎÃD~Žõp>üų'Ōøáė9ŧīĒž×™*š8p‡ú$øw4*čŠĻŧézųųfķ°^é5Gø/{ÃųķīŽTÖč‚ĪÃS>mô¨Ā€†7ŦŌ˙ÔÜ|ÕmČķŽ×ęĀ@WG`Ā+Šß9}§ų‡ÖTŠm0pĐ_ÎC˙pŲ`ĄlčĨΝōšˇ‹MZ÷ ā÷é‹‡Ë Õ:}-ÚÕ{ô°AÎŧõiۘčq‡‹Ēžoh,ē •ũÎ?RtߨÛU¨xĩ>÷„ÉŦĢ?~ĩS"Áŋ×Ņ=„/šÔ˙ûb]%z ÷XŸ™Sܜ81šwž6U_ŧrĶK$všŽąmauü?oŒjüō˛æZãI†īĮs@Cį34iĪ ¨åAQl]Š-*¸g.2jŪī%Ésgp(Žģđ~8Ū´ IÕ)ŠĶ§° ^đâÍRŋĐyGRfFpG„nI Ŋ}gԚøÚÔw§lcA e‹×GyũYÉ3—¤,(ąXYzDčJå ą(dyæÂŽ<.‡Ŋ]Ĩ™MœÚåŗö <įmø jÁæ[7-Ÿ“gĀņŽX˛Zöw‘iiJķÚĖ…SŌX€â‹N›0Ęt&{yZ5CqhŽ÷Ŧ%‘bPsĸųÉ §ōü°Å̟U{ÚŅ…ōVĖô^”1…ã=oËfņ}Ŧ÷€ŧ(å?'~ŒrÕ[ 8|W—ŧ€q`ÕofķWīëv` xĄk––/_>} Ú=tĨ2TV{`U†5ú“YŪ"ŦXszáō´€Ī”!öŌÎ]Ւ%ĢûŗįÖ¨Zˇt×%}ŖŲš:wÆˆĐ ŠąžŊ:˚ŪrōB–EĨ,žģĮÃáP6EšV¤Ŋžnよu ĀõΚõŠĨ›cĶ6Ŋ='›Ā=d2R[Ęw.ĪXneYp|dË2ØŌÖŌíš&Ųún¯¯P{ÅP~1Ûˆu!ÜXüŪ[[/ąāø„-ÛâG”dAŒHųnäWŽ›'ĮŪV"i¤x›u曪ėD‰öÔĄ'į–ŽK™aøąi6­Ŧ—vn;ÂOé1Ēį–5ZŠ­sCˇ5bÆöĖX_ģ5°ÕŲÉ늯ԙšÎ.šS4fŅæÔI˜søõ…€ĨōĐĻ­ëL, Ę3xÎæ•ļYėƒ;ĢÛû^ä.ŗ…åųĪ\ŋa`TšÖÄG„õįGM8ņˇāDÛBŪ’”ŧ*ƒÁ„÷ß:ã=oÃf…ĐPŧ÷XC#•ą$"ÃļČ/ûā‹åŖpĨčÃOā3=TD=Œvvę÷ŽWÆŽŨōŪ)Xn➴9ž˜s›ŪX^bqåøFgnáKĄ{”ā­‡‘i6>~ÄĒEsЏn>ĮlįšÁ^úpkIË[>įĘ-tKf|Tęj}ęĻČ7­¸#Bâ“CHĖBč⹟ūY­Vŋā;žû§_õyPPĐ}Š•——O}ũ=>ŧ~õĮ!Ã^ 6íÆ­ķE˙:yËi ښ›5 7!ŗ…ŧâJ,ķâW>.˜šėŲŠØ°-Ū—lŦ'<ģCĖXüîÛÅ3>K }œÃ-™ĘMo¤ņļīŠ˙ķl €íÕöŋy`Į$q"qâ3ų°{B*?e#÷S*Âŗ‹ũä×7^uuĩˇˇˇŨoĢĢĢ!Ũĸ_´ŨjũwÍÍvpāu—Mņ'‘Đ'uų[rĘZîũĖŲsúŌX)9™đô`Ģ÷ė5'…<æĪ4”Vœšų—™ˆ˙’m''„˙"ėgۚŒ˙Öjšo4uvöÜĸæčč8hđo_7žûƒW „ß55ĐšúPąōb;žŋcáķŽd@ ũÃ~æM­V˜8qâ¤^Lœ8ņšįžŗ]@ <.âacŊøą jíÁčō偁ōt-ķČĩXÔųųĨÆŽŋKã¤ō,Ũŗo:ũŅÜ]—͍2\–ŦyD UyųK×ßŅŌč|ã“tąļ ī¨ž†ScÂeRŠ,<&UÕg›ēÜhi`7¤ ĨĖŊ=GwTАĘՌ­WåE‡wũũ¤ûk2F.W(ärE\zWŸĶå'F‡Ë¤ŌĀ@ixLēĘhsˆüĄÃ•ĻâOãŅDEx¸400Neék¨âd1}~ÛŨŪŊÕ`QgÅÉeR™LĻHžĶīúōxn‚"\&•JeŅĘ;÷YƒĨ4Ywˇ‹ķŖģYNWСė÷^#E#īR(ZY gú[đ†8ņpb×ũ8]Ņũ‰v×2EbžÖō¸ÚõŦzUjœBŽPČåō˜ÔŖzϝ:ÁčĻƄËd˛pytržöŋÃ\ƒ°Ÿy3™L'NüųįŸūųįįž{ŽĮŋ/ŊôRiiiŋĒŋÕ°æ/gjā4vœāúeCƒÁ#Ŋ˙o–Ÿ÷mĩgÎĸ2Ô´tNžŖũâū éÔQ[ômĘ÷ øÂ‰NM§ЃB˙đƟ^îüv{É @ûŠ=˙īœBüūw§žU^lwœ=C@YuÉéš: ŠMœü;§‡mԞä햲"ÍÁŗ&c@s$!ūą!WtT–üõ‚õĶ ĀŲuD666ЍēgķxåeoŨ'ž÷gɝšŽŸ9˙U3\_Øđۘ‘NĐķÍû'oâų€ė…#†ôjöúå 9ß\9ojĪ‹fūal°ĀņļžúûaÆSW*øüuҰSŸ.¨ŋŲÚĀa0Ø´ˆšØų~oo=S¤Ū­ž~wgc#Z¤L’x;×^>VcmœķÃ#ÆĪō`Ī&­gž9đėĩpt…oĪú˙9}Č °†riO™ÚAsíwĢi3šÎ×0Îh:lY¸~8ŅAāë?ŅáÛĸʍí΃Eą1’ā!ĪRČfÔ6Đžb.˜Ēœœ*ŲîīũšSŸŠ4/×2&B* ŽtGiÁoāffŅÎÍŸŽ‹i´ŋō¨ę‘ŊŅ Ú“į#SøsäyĨō'+hÕū•H.ĸ{ÍUS”d;ŽæúB›;?níŅCYá;ˆcōJcîNgdûˆh0šģ=G—Ĩ(ķM+ʗ UʤtUUü4æüäô†y‡ Â`4é36M—Ō”‡|åîT }A\ôƜˆād_eAŠōļĸqŗ %4ôyöŠÃ¨ÕÁW,xÁáéųáÆ‚čČÂĮWŌž:ŦzcrņČԂJŸŋ$:9_’-ęŗÚ#Bš/Í_@[4éŅqɅÁy Ån Œ.7)qŋēÖä1æž8Á™Gŗ¤ôeĩÛ7z)$%ėH”ˆ¸0Ē’g+Ķ/„ĨKúUđ†8ņp"ô‰I9ęĒ:Ē›đü°ÄŨŠÍhķâæ'åJ ÄĒúG×*/LÎ+ˆŅå͏IVIrÃöęlČIØX{T!ĻõGį'ĨûJö˙Īی@xö3o7oŪdY–a˜žūŊuëÖôŌ~ū˛ÕU8ȸQ[ŊãŒĀĩSĨ)_ékœøá/‡ēwÖ]Ôl(4˛wƒk›PøęHLķąCgn9{Oy<|GFLō• q|ōv :ËüķCĩŠmøČˇÃGúĩ[Õß~˙á…[@gËõ6á°W%#_õâ ĨųÔW§îî,møūō‰Æž+7CÆxû9-úo hŋņĪ 7‰LØ;ô`ë˙•rđĮķĻNO_ŅÄįŒ?ÕøqÅšģ–žyâÛÚʖN[¤ˇ:Ž‹B%Ã%î7L†ƒÎVļßG÷ŽÚoJ?P_ŋ'Ī‘|ēí†ŊuuĮĮg kZ=ÆŊüö¤Ą.7L…ûž˙özo›tčUĨ|­Á‰úĘ Ą/v6š*­}Ę|Ģ!'÷â)S;\‡ŒGëŸíįĢ[]ŅiÔ^TîŠŌāx8ĸí†>§č*ûŒDmš‚ôEäü`4“‹M ųI3äqyUĨ ˛˜Ŗļ5IĻ4AS`u˛L—-—Įåv­ZÔš ™L&“…G+ū+Ošŋļáp\d¸<:]ĶŦNīZfôG•1á˛đpYx´ō¨žmē\˜­IW[z­7§ËeŅ q÷\ĀhR2Š­Ŋ[ž„Q'‡Ë“SŖeŠŋˊ‘ېÚ$×UÆČåryx¸"1OËôŠ‹ũ€G›¨ËåōpyWNĘĸÉMP„‡‡‡Ëdō¸\mŗzãZĩõÂÚááō„#ŖM—ËĶĩLwåáá1ŠyYÉ1ŅŅ y¸"ų¨öta´ŲĘBSmöüđpy\ŽîîŌ~oë=X‹*QŪUģ+ŪRĶiŦŋÉļw•4N#Ļ‹Û„ͧöč[/×ÖĩģŲļŌ9‹G˙ī†QíÂÖMßĢ;šNÔ;ü_Ā‹Ūßč§‘GĪ;ÕOŦQēö@Ŏ×mA‘ƒëËc—6Žéøo˙a´„ĶégŊšrōæų3M-c<ÆŊũšˇŅXYoŊ1Fĩ˛ƒŠģŪ aW““‚ߟČqqrtĐöíA ‹đv¨Ô2§N5Í 1œēĐ"šØšE÷¯Í…×ģėč4Hūv€‹JpöŸ2ËÃĩŊÉ僒c-†Âę[ãŧģĄ3ƒ§wvvrvurŒ‹˙ŨĩúĢÕÆ›70•×ZĢŪ ŋ}éŪū텛<Ã'§NtaëÕņģô­€–j퉀ī7m¤Āéy—ęos›ŋ­ž5QPŲS<NÎ.7ŋ€ßūƒs›*?ŗ/ŗ7ĒO1€Ŗpyü„qēŪy{xž3yšāvbķ÷ÁkC¸×N}÷G­mFk+†QŋâĄĮ5Å{rr‹õ‚ÉŅķ˛ŽHm+¤ū+WNVeËöåÉ`JËú˜EđåkvČD0æGĪČ(Sė1G“ |˛ ˛üš0jTĩ2eT~‚%õP˛/ 0ęũˇ'8û“66DåŠä"č âĸöûæG_–˜&ĶU‚<Ŋ8*O!ęŊäŧ&=\‹Z9C™q(ŲWŗŖ YĀ…íŖũŠ‚€1é鰕y+Å|.—^8%€ž a~žde˜€Ņf%íá¯Ė+đįÂĸVF¯Í—å)ėéb7áh<ĒLՄe¤‹}~ĖüĨ’• sŲ„‚Ŗ2.Ŋϊäŋr¤4gúĄÜp.FÛŊ|—‚ú™›•ßc1ģģ=īí0v剪R%ˆ¸ŖÍŠ^’­ KĢ7.ŲC­Ų§’ `ŅĒ4DŠôÄãŅŗ§H‚E“45ëŽRLibø’ã֑i;îMęö§—‰ã÷ųŌ@Åiߘ *?wãülÖ'l^lL¸/÷Š EqDŒonną^Ļ1Úũû-“Sƒ{7ÖC‹ŠÁBųpmĘR\L:+đā\Ąąxŋš+‹õ؇ĢÁZļdŠ”W•¨Œ‘pûŅŌž|IL<ׇßM!MęŒų‡ø“×ėˆŨ§ā=đĮħ%ørĄĪ‰ŪXŧC.čukÚ°C&`´é‘ŗķƒwįæųs-ĒyvqŒT!zĒ÷SâÄ_§-ę=Į!Y#ļæ>’v])Û5kŽĪ›r|Ėä1h+3īܔ{ÔɈ|¸úÂãē0…˜†ÅÄÂb˛ļüŊI IDAT>Ŋz™?yåĄ—ôųŅŅea;d‹ž NŪąĖ‡Īåré°šŒĒäųY|ĨB }~RģŖ@*ŖÍŠNĘ ;”ØŸį5áˇŧ=v† q´`Z´ŸÚ÷õŠģ#•iîÍGŅ‚@Kįõ[OąŅööZCs]K×Άļ6ë`úpë˙떡bÚĐúĪ%9ZppqĨ{g“\„ƒ‡rlág÷āÖŲ/D4X[ãrõ14A ˇˇZŦÍ5wī7ÚÛÚŦ 9ޜč‘ ׯߊĘA$äáØ ;>>sâ§Áƒ[Ŧ{›ņ§‘Ã{Ū#oØB2ĶÅ÷˙rņ·­× č!žŖDöĸ—Žēæ†Ąđ+C!Wá˙.ōīKæ`@0tä€Įī9Î.]ũÔ€ §+]Üö+wŒF9ŋŠØu$\ü°Ģŗ4_â#ŽXL[ôXt…ZQôÛFKŋLčėÎ4ÅzßX™Čļō回Ŗ1Fų€â‹}§˜ĩÛŪļŠęˆĐ+ŽE—n‰ŲīKėCÔ ˆØ}RAĶ€EW ŒITú¤KšhĒGßāú+büīQČ?šāL˛ąT9?nŖčōv$qßNE üm‹ "‰ŒŸSVËČtĪ[ĶíŅÅŅųøpP\F˙ôĉŋ>'2Ú܄dmXzŽ}C<Šv]—höīŅMÎ<ÃUįįääįėQKmÅ{Ö韐™ž1!<‹åŠÄbÆBK8OBŗŪFcÕäfŦ-Ķę-,Û`囀(iT˜äŅåĨlÔGíXãĪ…QUpĄÁ”Ŋ$:+Ä&ü?¯ $x{Š8 \xũ>pÖđÛįV´;z8áF÷ĢnŲb*'ŅÎĐŲCöļ[mmõ8p—$˙S÷ ¯ß ´b`¸bl`W¨Đɂãl¨>¨e€!˙›øj0§ĩ(ûÛĪLũČÂ_\_xãÚÁ‹Éš\ĮLūėž ņį8hėŧaŧŲŽk;Sk€!CėœëqíÂå7ūˍi譟ÖüĨĸæAēv:ÚõÆ6 ŋgĮŠ‹€nÂUû‡‘CģēC'8WAņĐŌ>2eÅȝŠZwõÛSúēÃá /ŋŨ‡Ė.íÎĀMX­7Ú1Äéđaœøl@û¯Üé“ŗ';n~qDTL”Üŋ¯ÅZ–ąĪ<ĮË#œeY€ĸŒI‰ų>ĘŦ<™ˆÖįEĪŋ`7ØQ­MRIÍíšĻ[Ā—*w§vĪ61ũ=öˆ(ŸØŦKŒÉ‡iĘĘÔjUvtz~ęĄtɃUļĶÉúĨË#[O—›Ŧ’Ĩ§åIĐĻĪHîĻŧ^áÁ§XSWôg1ą˙Î^! X‹…íū Öå§Ģ|â=‘4ėČi)UÆeĶ+wės-Ēų~ģN.Ë)æÎ;ę/âB”˜)2…'RŪ‰ÉÂ<6Ā’1š=éU’•Šâ~gôęÂÜÜŧãzQؚQašą}üŨŽd^ģ$§˜¯ŋāĢŧĪ[OwÔķ=¸]×l–ĩXÁ?`ŌgTĨÆmŦ•īČęŠNŠ‡ŠĄk?)¸b™Âwcē΂ûĪûûŨ7šŖ,5ĄkfÜī‚ļßw,‹Į˜b'>N´šf%$—ų§į&Ū?ųÚ1šœ&"O*A”åÃĘįįjâ%Ũ^6ėV'×WĄĖUôyŠhȞô[‚ÅhŌã66ÄfeĻú ubdz1gvâ~ÎĘŨŅ]7;–㟸㞭÷î(!ü†ųũÎ›Ķ i¨ųVs ¤öŸg~,(:û×Â+7nŋĩÕV¯ũđĐé ÛĪVgu<|€ļšŗ›?;ĩáã‹uŨč]†9TYķ`8ŋķvPķåwË?>ū~ŽūN˛nČËŪi Eŋģ¨ęÛ3ĩߖTî=pú€ĄwĒŗíüßŋ‹Û~ļčōk]s ô%ķņO-?fø×ŪŋWļŋgōáøė„o"i´rGÁ‘T_=?\‘˜Õû¤=ŽmĒ21,Uj=Ûįŧ…ëæĢĪÛo;ZŅĸ-ÕAņiKCC˙0žvŋJzÕ ?Ėŋ_gB°,ÃÚV rĩĸ‰Āj˛Đž2‰ˆŒē*ģã@ŸŸ˛Q•~įšė&ĨTYûmĮŠ1zĩJķo3q}Â|õ99]o{ĩ*•FĒŠō‘)b•‰2Z§ŗ‚ĸ9°T™ŌvuĄ¸”ÕfųĮŗcŅYšūac4`ĒŌYlĄ).L;jFøęķ‹u Āh‹ķõž>\ÆŖJåÅîC+š9‰ŲˇĪĀ„ņhz.ŖH”=ĘŠŊ:ŒŪŽœŒÉņdИ Xt €‡ąįŲ^~ąčÔj#W$Ļt]o 1 Z=<üų4ŒĨGKm3ZF[°ŋŠ/šũžž0Ŋ€gÎjŋ8 ?š'LČÕúÆdÉĪJ”KiFeŠÕ˜¸|  ų\ču&0Viz %Ú7*ÆCĩ6ŖarlīŨvöÔĄÅaVU¨ąĐĢökEmú•Ÿ•×ëœPF›—0?Ëŗ;3æÎöĪžj°§‡ļ´K‹ļ`–+ņį÷ŨV}ÃĸÉĪŊ}‚ĒEsTĨ52]qŽūR~˙:kąŊ›d,Í-d%r1 Kķr úyF_wˆKNėucÔ$G+Ģä™ģī‰ÜôGssmįđ>˛v6øbŽéx™í8IŖNgņđŅ}ÖŲõĨ:+9ŠxÉY+íedKÃöLƒÖd×ÔÆŖĘäRijęmŨū"MVŽíĩ6ƨQ•>É3 ŋrūS™7GQˆtÀ Ÿ•*kô5hŽ_ĀĐÁw3oíĩ— ƇÁĪ‹Ūžá7Ō ÷Õi#Ëū^[Ķh8Öhu xq\@ĀĖ §Ö25:ĢßËüÁ7ŽŨxÔF{áōģˇį\¤9|Átūb3—ÁC$c8”Đ-~Rķ_Oš*Ī]iņ}ŪÛõēēĨŋ õîqôrœ$˛a}ũ¸%̜éŧģčĘy­ž‚įGÎüÃčq€^§Ã ˜_}ú3mķ‰3mcøÔ?hT;ûEL˜yëė᚛ ?1^ĪĶÆŸ8Ņ.xÄ. U¨62pė>L.čÕ;ˆ^æqęę ĩ €ŗëWecŪövĻЇĖ^ü?EĶ_˙Ž¯ĢŠo}^8ŌF{ļúĐN|Öb8ą4Z)ftĨĒ^gĮĶū1ņ> Kf”r(ŽHtŋũvųeUJr¸ŒĨiđƒ—eJũÃæIķ“Ãe\qXÚöđÛ׉ŖŌuÉķeé h‘,1=J ôg%ĪtÖ{Ā|‡ö]柔4CÁåsųÜÛ.äJcå{#eé|ij^jˇÕwAøš•eIqáš hQØĘ´pmnR†5fwŒ¯k”eŗ“”’}é2ŖÉÉŽ’ŦLíO:Ęx4yIļFß`˛&ĪPˆÃR3|{ueo9˛DEar´bŸËåŌl—äĘtErę ŲZĐāŒ‰MOWŦˊU*gËĶ9D2ešB-e9q“,, ŽOØĘŦx[šÂRšžc ëžŨ‰–Ø+€ģ#^ōČk⯂¸5,¸>+Ķüi€–ÄĮŠ’æËķ9\˜k/Í/šŦ§[Âĸí-čÛS‡–.[#KI–Ë,_šnÜĸÉIĪįgÅöUûsĘŦÔÆĒļ;}ԎŨ žvk`´YK’‹uĩ&“:N^蟘™*ĀZ–ŗ$)É”,!3ҟîŗ-Ā~ß0Šsŗķ]'¨Z.ė߸1ŲIJ FĮf­´MXÜŠX]Nœ<Ûdaøū1iŠ.Ā4įdW%FČ}ûáĢ{d Nü-8Ņ[ÕĐ`’Ĩ>1YY QCqNqC•>Š-ÅÛqHé]av6|ĸÂEôÃhg§~ßø´„ĩkã"s)XnđĘôX1`ą['Ŗ^§˛p¸>ŌyYšŅžO&īÖĶČ4“˜ NŒ“rų\>×ÄÚËfo<ŪĀō“"‹Pü°´Ũ‰Ņ™Š )JšĖ€+–ŧĘ@ ØxîįŸVĢÕÉ=[Ž<tŸbååå3gÎėņá?üđâ‹/>ž<]?āėõʎˇ‡=KûčlĮlĀuä†÷ƎüO„ĖŦĄōÃĸā8;ˇ3zŠŽÁ`IČ_#Ü(2ž2Ob\üa´éŅÉHŊûԁđ 1cAĖė¨};Âį 2RÉßŅ÷I1Or`ū‚mŲĸUœ|TAg ''ūęvOHå§lä~JÅãņČŖáÆ~ÁãņĒĢĢŊŊŊí~[]]MēÅÃŅŪTTbā1ņ…‘˙Šd':ŽM5ĩā8ĐOâ;‰Ü˙ečō•Y=¤ĻŊ• R.1á).QėÉiVʏWMƒĒJ“ųËLÄÉļˆ‰ Âö3oƒAŖŅ455uvö|÷ÉŅŅqȐ!cĮŽuwwđ đ›‡Œ  1q"q"Qų“ФXžmúĖŧÛũÖŅŅ‘Ãáہ@ Â8xŖ(ŠĸȖ:@ @øoÁ˜€@ @ HđF @ ŧ@  ہ@ đ_Bׁ%MMMO¤ē'Uđ,AƁ@†8‘8‘¨ü āææFēâo"xûŲyČéÃO¨á™ēˇ“qA !F N$N$*˙\ŋú#ųˇg˛m’@ @ HđF @ ŧ@  ہ@ @‚7@ @ ā@ @ ž%œė~ús'ĢŊø¯ëM掎Ž_9::qsķ=ū9ęÛloúlûÉrÆiü§ÆŒpü¯3É-Cú‡gj1púÂßËw˙ĸCWøu†ļ˙ßĻčį]ėmģrúΟ7ļģ ˙€áNOQÄī?ũfß5<˙Úk7 k:ęŋ;ļš‚qũE1Ėš?ĩš+•ģk˞čCF Cƒ@ @ø5o—/T8:<7qâDģßVUU]žPá7vâoÍXG ķc§Ą.ΤãžLŨ‘””Œãđxķã/{ĶV‹õlžŠ$`-›™áŋí@ŧį3n:Ŋ*Oũâ š'€ŠÜôF2ģåāj˙GYd2Ģ—đÃ"ũ9Ė‹fˆ8°/â‰ũjSStøJ@”LØû+ãɌå™ĮĒŦāz…ŽP.“Ųoŗ.wŅÂ`īü?¸ųXjēõœLŋCI[ŠjÛ8“ˇNG­Ûŋä=õâiã¨'ltļŽ`ëĻį­X¸‡,KŽ—ō€ē#IŠšę+Vkø/GŽPÆËxŦfËÜE_î í6í“Ŋ+üa¯ø“ZĩhÛĨ†&ķ˜5…;&qėUŌŦ=oØm˙Ûîöî­Ģz×Ēĩ…—, øņi˧‰Šûx‡Õ6˛‹æ”pŨB7¤…ė_íŋõã(!ÃąMŠŲĒF<Ųâõ+dBÔdŋ•tÖĮj0[MMœŠÉ›Įqî]oÎ~+éŦ؋2Yī^ĀTfĖN.2(q؊-‹ƒėŲä9› õŽU•Tņū´Yzę¯*3X -ŖVøHÎ1o]—}Ū –Ĩüĸ7$Īđ…}]ė<įōÖfŠbÁ‚7yŅú•“x–Ęŧ”´CUV0,å3sũŋŧuį­ĻĘšĶļQ> >Úâ•÷–[vŋ¯ÜQĐljâMž`Q•×ļXMĩlËę0zé`ÚĩŠ°ŠŽZ:}å9kÃzqf×Ō~oëŅėƒT°–$-ŪTfpũĸ7$GûŗĮÖmĢļļŦzc&ÅģbwJ÷nPWTå>mĀW):XTe ąž;˙ë íÛb})Ģzëģk­ąŸ¤„ēe2Î,[ęÉTnēŨs†˙ĪøÖ͘‘ē0Ō=tKfŧī¤ģ'ö/zW}¯=ģu˜Ík/õ”“ÃjwŊˇ´°Ž@ e Ö¯ VíáŒuËõ,h^Ā;kÖƒe­ EqŨ(ÚL€0¨+­h5›XΘqŧ{lžũp¯aĒ2TčígÍe™Ģ–ZŊ#fÅĖ’y?‰I•š`ÉÜãs§M l=įpæ„;yŽŧK/Åī]/ã0ė_ôŽzΧiŨ39vÔåĪRŌÍc8„aozgX9CœøuĸpZÚöPíļšËģ}&šjKHŌ#|8yϏG~{`-W š‡CëLõaĢz×fæG‰l.ˇÕl(;Î_$Ā ŠËnpüŽg8:x€SÜÍÕëw_žÚŅz•Á¨ž_4]cĀÉi oˆ÷čĻOsnsúEnŒųRņÁÜ=*_:cŪ†R!~ËVHK>”~ŧ/ŒļŧŦ5dūÔ;& a>2{AvYÄ2öX’˛Č'õ@ĻæKĒ+BYԌüë†]Ë|)€=ģ˙ögŋ2C?ũãī„0Å-~˙‹ŸFoōĸõrOĘrōũČmĮĸļGŠz/Z'­ÂrnĶ[i9ģ–ųŽˆŪžw™€XĪŽ]°éĀëâ…kn BWl[&æq¸ÔÜ)  ’ß=Ũ-ã`jē—īRPxî­[ˇ},åąš-ŗRūVļĀ“î­Ë‚ĒM>[?°Ēúļ^ŒđA*p–m=,âQ€š8yî&U螈ĐՋķŪ*˙RĐ#ReÍ,%ėŠå8<Š57°”tÖæ•įį.ߐ)-Z[˛!3¤ÛäŌ\ŧ­ˆ;į#)§GĪŠn­XĮŨđiŸeÍ=:Œ]9…â7Wą@Č˜šœŲÉ9ŲjņšŒĨŠÕÛ ĨˆŪ§ā=đũbĶxsaČ_˛pŖ*hG¯×­iõŽI<Ļ&û•8{e IDATÅG‚ˇ~´Īc9ų~äŽcķ&ôēĢ''–syĮ°zՏ xaI+TKß -5Ų #VlŊœ÷°k8^ÃŗŋĒĶ[)ą4zÅâH_ĘZeĨøˇKpx”ĩÆj›å˛?lzãõ p„YėęÅ!O$×Ûȓ—|!äЀžpáėmåai!́°lûbo>‡ÃĨBį0—$/Íá'EŠaČWæ°s>82ļ:{ņĒlŲ§Ëúķŧ&üvƒˇ'…‹÷˓ĘËO6ĩ”ŸĐ”€ë¤é!3Gô! Íq<Ā@;sĩ@{Åáo*îNl˜Vt4ĒË2T×oN.õ]::wĶĘy Ø6éĩßŦŋÖrõNs-°\e`аá]įs89ãvNqŖ#F=ƒŽŪai'ŪpˇŽ-ŽNœáƒŅf ŪSų5pvĨģ]lŽ8šâŽ>mÍ-÷VęäėN­°/j3ŧ;ĸvK@Ũ×&í×ę+ëęÜ3´prį9€ gpLcKzÖÜúũßOîĢa‡.ô] ô’ ˇoũ}ôw=ÛOē ÷Xŧ—›_ ~aä•šÚæÆožiü€‹ûŧš$ŋŽm“LåĻ7–QSWgî ?ė[HOâ%ŽįHĘÚ`…åJQ•ûŒ ~ā’ņ€:ģ3K_7zŋ#€0$Ę+{įes”(ž§@ =š-u–žk  8‘<āú…úXs.˜áKžŪ›Q|Ū`bYK“5Øļ,JyFŧęwÁjw­úŠ˙l–7ē˛Ōj=›ąč´mÃ2”™ą§ 8v—Ÿ‹ĘŽTë” ÷Ø_)Ą‰åøøq2ļ¯Z{9düØ Ų!`íÛ`] rGđ8î><ߋĮVšxŌŦ=]úgŊáU°ęŠs7•^Ō7ąVŗ•ōbžŗđ’ח-~ī­Ō ÕÛbģŋÂg9—ķacȖ0áCO˜jzuzËÉ6Íɨ?š9ķ\p­ŗšáF38ƒ´ˇÖˇĐî]ß‘ŋæĸúFoŽ8ûŅŋ‹æĖuuĀĩ΁âQķ‚wéĶŪ1p¨#Žu¯Ķá~ĸ^pnĸÅÚ܎AŨÖn:r›¸Ŋ0{„aßų›åE_™đ’ÍsZÛÍm9ĸÕÚ ´ģĢãđĖ–TkģZ}¤††Ė["qmU}rėp“}ŲĐbûOįM¸ėô@iûÄNåmF.‰ŲxŤģríd…ū§ÖÆ\n•H\~ ãŽö[öÉīs-91=zVبžVûöiÉ@=Ÿe€ĸĖĘUų^+2ˇ…ˆ(ÃūEījė]k<šiyé¨ [ģĻéVđ‚“>Jí~`{ļŋÍԋ1™=–—~úEeyŲšŗĮw-ĖøjõʀĢlį)×/]Ųzēī§”†¤)?–đ Ũ67åzõ wŚ-€ÕĖR<Ē›äŦÕĘvP×åo+ņYđŠ˙“xrۑĶZž6)‡ZúÁv™'×Z2Î!ģmŲŪcÜY‡ũ…ã3ŨÍĶRUM_qW$J(“ 7¨ļĀ’ąšƒŲUc—mđėGqÖ .ÎÛķˇŊ{čę-3Â)rcq_‹ß™Ā‹žĘžŸŖâ5Tzŋ“Ôw0|WOž;ĮrĨkŊ€ĩZáęų€<Ššdãō ŨÔnoŦĨĻĒ+BāxĘ^÷Ū´Ŋ΂ûĪûûŨ7x^ėÚ3ftÍûû]°…í÷„Ëâ1Ļ˜Ä‰Ī€í†ĻKRËĮ(?ękã_Ī ØK;‹ŲˆmA"!D 6û°ŗūtđRü¸nĮątŗŸ+G <'‡yfXĘۇÃVŨ^âŗšYNWʔ˛Õ@ ƒĻrR*Í=ŖŲĮ¤…E1•Ų‹ļŪIŨœęĮcúÜOkÕėZĩŸŗė“éž6X×QËŌîŲĘqīŽÂo˜§û;oÍO¯Ú^öųųĢ?ŨÎ2q‡ē8;9ģģč<˙]ÅÁīĒ/ĩôŽžÉ$ƒÔžĐ)¯ũūÜß|wöãoŽ4ˇ;tĐūīŠĘ#ߝÛw‘yLņ‰ŊĮĶŖĪÚ[ļŗđtæ'_˙å›ë­ũ*ę(ÎÚë˙õ—ŧīs˙^ö—íĒ}ēÛō¸püFš)vZž)üąąŨņĨāá<āĻîōįĨ?|ŽVUz1÷ˋÕ#ū ßĐúcî—gķ‹ūõģQßlâ4xčø)“\€Öú}'šÚœÉFģ¨=Zļ­°"ķāåĢ€ĶpīņƒšÃ€ë‡ŋ:›˙Ī+ÍptëɏU|ŨtŸH„ãN¸ųŋ—íĖ?šMÛ~ģ=´ûŦŧ­˛HõįOĪŠĒ›Í]ËĄ+øĢ€M˜ą&íĀ—É‘üs9š97iWšąį5Ę\ÕİԜÕ÷ũ*×+Ô§ņĐÛ~zkuiĨļz”āšĘĢŪ_jCɞJŪԗûĩÄČvŊËęŠķĒܧIxVS å# Q€šŽĘn×0IÉ4D­‰ŋŊ5H85:žëÖ ŒáŦĒŌÜcqŊB}swž´1kO–ča֜ŦĻŧBŗâ×,Ą¯ÔYAŅXjĖéģēPĘjŗüãYĩ\ąrũBĮđ(Ā\uÅjĢ‘Ļ80˜-vԜæĶx¤¸ŽX­ęˆŪkš€š8mĶ…×?úb)'gmŽæöäèĘŪĶšė‘gėÕa vädÍ&xĘ=š€åĘ% €ËFYT‡J­`Š;Ģ6sD#(ęŦ؆ÜGņ)O+­ŗ[]x¸š?îöQr†ĸŒbŪ; îĖYí ÅÛŪ‹œķūžīyJ—{¤sŦuš8"P|ôWĖ`ŽžĐØk(QŪQ3…Į͞¤1ÁŊf’öԥIJļ´Hc`Pũ­Z °Mŋ söŸ4÷J Z˛4Į2ķŖĖYwNÕëĢ{ŗØšō.ŦՅĢšcGņûnĢžaÕæåWZģūV•hÍ,ĻîØžsČë_§b­ļŧ´š|O1;>Ė“Œ§å÷ķaÖ]âÄߒ{É /^7;­:"õŪČÍp,÷Ā1}ŸđFrĖĒ3ĖĩWŦ^BÚž<#¤TŲÁ=ĘTuãŧšR…åFæōüķTđ!ũéÛÅ %{ΰ>Ō'rÄK/#[ /(؏° 5fģĻ6Ē6Ĩœ ڐ|{ß&oT„ûĨėŊg-Ā+KJÍ nĪíŸjí=„/šÔ˙ûb]%z ÷XŸ™Sܜ )žęĸēÚk'¯YŽößkŠīü>}ņpšĄZ§¯@ģz6h?ršgcQ]­ŽîûŅX§ŸęÛKžÂŲsyßTžÔ™4Ííę?n 3nö§¨ŗhė˛?ēūîĮķ ×*§A|ŲPįnģ+Œ}m´÷nMuĶåÜsÂ?Kn_\ßXĀ‘~~ÄpņCŨ"ŋ¸đõëéŌÕßôpîŗ-Ętę‡Mœøōiî‡Íį+ŽŒ–)~œ0āÜáōk•Ú8Ō#}Ŋfž6bWĄâÕúÜ&ŗŽūøÕN‰Ô?fBķÎĶĻę‹Wnz‰Ä.×5}ÅĩNü鯉‹ô?5™tNî~C›+ŽõyåC{°ĪĘÜŊ‡ē̝•Ÿ7pr$=ũ…_Ũ8ОAQ˃ĸØēR[TpĪ\dÔŧŪK’įÎāP\wáũ2pŧi’ĒSR§Oa)ŧāśĨ~Ąķ¤ĖŒāŽŨ’zû:΍5ņĩŠīNŲÆ‚Ɲōúŗ’g.IYPbą˛ôˆĐ•Ę" bQČōĖ…3\y\,'Ī€ãądĩŦ˙fâMKSš×f.œ’Æ\tڄQĻ3ŲËĶĒŠCsŧg-‰ƒō˜ÍO^8Ĩįŋøƒ-^ũŦÚĶŽ.”ˇbĻ÷ĸ¤ˆ)īy[6‹īcŊäE)˙9ņc”ĢŪZĀáģō¸,āܘˆĢŪx3›¸z_ˇKĀ ]ŗ´|ųōé{XĐîĄ+•Ą°ÚĢ2ŦҟĖōaÅšĶ —§|Ļ °—vîĒ–,YŨŸ=ˇFÕēĨģ.éÍÖÔš3F„nHõíÕaÖô–“˛,ĸ(eņÜ=n‡˛)ʝ°"íõuDŦΨwÖŦW,Ũ›ļéí9Ųî!k”‘"ĀØRžsyÆr+˂ã#[–šĀ–ϰ–nĪ5ÉÖw{9‡’Ø+€ō‹Ųž @ô¨ËüÆâ÷ŪÚz‰Į'lŲ?  $ bDĘw#ŋâpŨ<9@H)Ūfúæ(;÷e{ęĐâWK×Ĩˉ°üĀØ4›VÖK;ˇá§ÆôU‡sË­ÔÖšĄ[€1c{fŦ¯ŨØęėäuÅWęLMgÍ)ŗhsę$Ŧå;SW-ˇ‚å#ŨēxŨg[€ũžaVĖ9üzˆÂĀRyhĶÖu&–åÜZŌĀō–Ī9€r Ũ’•ēZŸē)ōM+øäŗēxîįŸVĢÕ/øŪ@}ũÕįAAA÷)V^^>õõ?öøđúÕ‡ {Ø”@x†ÆS“=;ļÅû’õ„˙ŸŊwkęJ˙ßrŲ[%á’D ŠBFRåÛ •ČÔ]ĻįWb§Æ9JŦ5Ö´´#ØŖÁGcA‚§Z…j ՝įT˜g€ļ6V„ GŠa PCĐ$\˛÷āī€rI¯ĶŅõųÃ'îŦĩöz/+{Ŋë]{ņė1sųo—/:”ũ(ĮPĩ;ŪĖ`íŨĪ ķ)Ū Q­ā­ÃšsȈȈĪäÃî1‰ü„•<Â^ąX,@<ģ8ĪŧųúąęëëĶoëëë}|‘[ ˆĮMSŅŽŧĒ!;Š=ŽĄķOē>˙ )2åQškŽŦ\ŧķéLğæŊ‘įᙎVsË?ôēŽöÖŪŪĄĮЏģģ{ûø BÂüØãīŋ@<÷ q@ !†@FDFD"?ĩ^ĄĖÛŗķĖ›—ˇ_đawķ÷‘ÜŨÜŊh@ @ ūÕÁ†a†ŪpA @ ˆ_ nH@ ‚7@ @ ā @ @ Pđ†@ @ ˆ_ }–ŧ`o{,Í=Žvˆg 4.4ÄȈȈH䧀ŸŸrÅį"x{,–nmmEƒ@ q@ !†@FDFD"˙Køå—_Đßy{ļAÛ&@ o@ @Á@ @ ā @ @ (xC @  ہ@ âYÂÃéUšĻkjjŦVkOOΐ¯ÜŨŨũüü„B!†a˙ŋûîŖS]ņĨÜÄ=îi,ũ>õgĘ3č•ܡĮ ĒuĢyËŸi€Ņo¯}}žī¯Pܛßį|ĀAŌų[§yŽ¨Æ ‰\V(5Ū‹Îú Ÿüocmg/ūÛˇgúW”´ƒŋøwģfļkš‡ĪŸlé˙9ĸˇM•ģ€ķĘ̟Ė÷A#@ ņÜâ<ķĻÕj`öėŲs†1{öė^xÁQxXlĮKūYÛŲëÉö ô ôōÆN;ãĐS¯9˛ĨpFhāXĮã9Ķ e(J–DDDDH2õÔÃëW[TTiîû\™ ’dž}ÕËTņ>Q:Ĩ˜PčRƒfēHgëû\,ÉŠĖĶÄúbu™ŅōĶåbB$"ÄōtË{T2QÄDI•Ô`Ī1”)Ĩĸˆˆ"YK9ŧJ-÷}~ÜūZŦ”K$RŠD"MČėķ9CQ˛LLˆD"ąėqķcÃ%|8Ŗ*˙¤ąwĖÔȜE nū¸÷ûŧđ˙ą+ŠŲvųBŪwWÎ[ēÜ8/ō˙az$Į ­ųpiŨÉúŽöœ!$"ū3ĸˇĖIãžˇĖŅg6tŲŨq˙Qä­Ú ŨÁ SāŽMŧ;)p”ËîÛIĶ‘Cį7tŲqīč?ŧōîäĄEiķ•C˙Ģ?y­Ë0†ÍŽ~=|Éä1Đyõ—CĨõ?]Ŗėž^ŒĐŲ/åcwvsŅ%Úp:!ßdw÷ũíäŪķ;ÚŸūģđ…žĻ#Ĩĩā45uÉ$žë¸ĻŨ ˙MŖÖŌëÉæ­Xū*ę ˕ģûtN™ē&đŽOžûę=‚#—jWfęŧúŧ˙­ĶZzÁ‹á‹l7æĨG ŅÛÍīs4GÚė–ļÚ[ŋy›ã^o¸qŪáĐXúŊō\7EÖ6Ū^ā÷ķŽķĪ?žũ3p~G|Ål3üã@iũų–n;¸ųŧČ{{ŅôH_wpz}€&áüÅļvđ†˙)Ö˙ו7ĨĖúf<„ĪĒ./¯Ž8đCrķQÚŗTĒUļią"ĀåV? †ŖĒĸ…R įQ:Æg‰ÍŞ%.¤3qhívEy`zqŽ3­‘)Š„jĪe ¸ŦōëŒ0nĶeĘ%‘j)Įæ´Ę JI.Đ6Zü§ j™U–-ÂīÛW§ž1L ‘”›,ä1ÁŦQ,Uf^ˆÉލâSņ0"‹“Sō´uÍxÜ€Îŗc’¤ y8ĨW'ŦHQ‰Š“øƒĨsQ ŪrL-á üŅÜĒŧ0W],ãeP¯+4B•˜ãL9¸ŗęÎģ‡@üÚqžyëęęĸišĸ(W˙Ūēukdíۍ¤{ Ÿ-œ īÖn19|ļļÛ34Ší pŗūJS7iųžŋ›Æ¤¯ū_ꑞˇô„đfŋčfžvõŗ}5įnÜēžģīL‰žŖ}”¯p*7tyūiwŪ8ÜüņđĪGēėîŖCųŪcîvĻĮXQ•öŊЉb§ōBGQ /*_iģG÷[ŽhIÜß €ę8^Xs†Û4ļīėņk]c^äū6ÄÛną”ŠøōjÜjūLuņä5ģ˙ÔĀX!oē¯Ŋū2iwzņž]ęi͚Üxl7ęü÷?+K,ĀaŒho¸xāōŊL`ÖoŒō渃ŨbĖ+lŧņ0žáJāŌLäĩĪT—ĩ–^đņÎÅí=ŽZöĖžāx8‡ÆF]s÷™ü›ŲŽŨ‘lžxΤ)Ąŋ™íā9ž'~E áĸM”ų—ĩ-Ŗ„sĻ.žŠˇ_ģú™ĒÖØ ŽŽĐ$đÆãŨ ÚŗG¯öüjĸ6]qf’tÁŠ< €ŌmW”[š‹RIÔu•I„ŧĖąäHU&ōb3ĨUDBr‚L&“ˆ% ĒžÅB›V•$%‚ Ä2eŲ˙Š•ÍGˆ%˛L]‡V!î[όeJš˜‹ ąLYf¤(}Ļ„%'Ée2ŠX,ĪÔچ­7gJYR ”.]Jˆ÷“;ō%”V!–(Ō“eDD„tßŲr‰"""ÂŅsc™R.‘H$bą4Y­§\Ęâ<āŅĒ’Ĩ‰D"–ôå¤l:U’T,‹ B’ ŌwhˇoՒļ.‹%IÅfJŸ)‘dꊁJÄbyē:[!—ɤąTQfg˛Púe‰Ĩ1g…X,IPî.í×ŪũE°i’%}­K’T:€šlkfy"yX,UTÚu%z^lL€‡ÄHyē’:ePÉiļž›6]*Q JČQzUĻV˜(åđœ÷?ŲĻ(h´hK%˛L=BŠ*V'3‡čs€Ã˜ô(}ļ\Lô—)v¤lzĩB&&‚K“‹ @Ņļf 8“á8Āa<l Ŝ&dšARڜŋFäI2ՅšqƒŪˇāb!œ/ fŌÛĐÎû8#ąšm€3˜0qüžĘšo÷quΙ’EI}ŋĻ„4šHOõ=[ÅIé š8"BŦ,˙*IęxfŠER•˚ĖŠD"‹% ŲZ۞׈į;ķfˇÛ)ę^dģŨ>˛æ9 ‰¯Ū¸zŊŪÜÕ>ŠĒmšq“"$„ō' ŨoüD™~4õøˇ`üofûöԖÛ<ƒ^N]âīÕŨ:擊ãĻ’ú[ПėĀšëgÎöÆĢ=^Ŗœ7ĻâÆ^)TsĩŠĶ-pöÔ%|w€›wnë˟ô;nŨOíŊ>üI‹_÷ÃĀŪÃŊüSøđËæûôÔn4p^ _Ax{Ū]{ųĖųvĶmĄ34N¯O[8P“Đúå'Į;ģÛza‚ûŋ:j+ĪĪS•9seËŗ‰+¤a7ÎÕä_Ģ% *Ģ\Ė"ؒ-šĖE˛Eģ̤šU–’\œ]œÆŗNĶčO(㊒l酊€ŌôOp Rļ7ĮŠ5‹dI!Eql"9CÂĮmš$IfyœZĘžäŧ%SĖ›VšH™[¨áËs‹&8.H‹“ü(‹Ų¨ŪČg3™ø{¯%€ą8iE‘pc ‡Ōg§äŗ7Ē‹Ã˜`Ķ*e[‹ĩԙ,NŽæ2eē.&ģ8“`,’¯Ø^)ÜØŧ]E'—L Œē:Ė;lãaeŪÂB•˜ ”~`ũ>jé‚|[Ö×j‡Ō)( â$>>\–Delš2ø@‘Œ6kíÉũī'S˜| ŒĮÁĖeI‹ļ—Į¨Ĩâ-Éų‹4ÉĮKÁƒ~ãõÕf˛1ÚŌLá"yÖFíĸ”íÂlĸDyH?@ \u.ß]Œ? bņũ͟Ėėĸ!‹Ųõ9Øaœö“ĮK/Lâ1(}ļlMŽ.&¯Ũž&ÛōĩFÄ›^ŖÃ€'ÍL>![úZą0’gŗˆŌŗīEU&‹×œ c3r§ ™UüįCpÕņynąÔ )Rm_‘CĮ,—‹C˜Ol(ōcå!*Uš‘ō(}Amnzäđ› ĮfiļaÁL‡°ĶĒļh IDAT“ p˙\Ąšŧ@Ë$âũčkŦZķš&?2.Y)2Gp§;žĀĘ™ÁėéŌ­8ÚĖžģ%7–wŠƒ`OKĖH a‚ąH.Û^™+á ûiJË%8”>sÁŌĸČ*uĶĻI’ä”ËERŪũ=EFü÷4ĸM›„[øø Đu[ˆļĶ_“´%™āGŧeˉ ¯˜6w4ķ•Yw~)gXõ'Å0%ŗįn,Œå1qc‘Lļģ*&—ĀĀf¤#šë‚ŲL&yĖŊlļRĘcQJ6Ÿ[,âĨĪ–ĨdĮ&äyx~ƒˇĮ؜ģīĖÉvđđņqŋ;C÷`ΟėņĶÅníYK¨š ‚fs}Á~žė.à <đ@@'´ĩŲÛoQžcûˇ8zNđtÕ8Mļß`šCfįˇĖāÁãx0žÍpËHöšZ›,ŊũĨÚíŨC”ÄĩŊmˇzeĻĖƒÃņˇą\ēėm”ķâ~ϧ´—´ā4G”úēŗ‹„Ë. î€įw€‡šÜËdôˆųúų€ņ&tˇwÃoŽÕhowaĻ›ā0ÛČ6É-dķŧų(.eŋŅÖ æŸ+~Ø%ĘÅõ!šôāčü×įÛtĘ+J°Ø´ėcbūƒŽÎâla0€Éįã6Ŗ l†=O–æØhÉ #8§3]š1$žā96ãB2ķtæ¸`ĀØü`0>Ķf°QCÜ0Ī`N‹ ļå\°@Ö\žˇŊ\k´Đ´ÍbŲ(đĀų’8qoĀâovJļîky†*M‘ŪžāˆGm4…9Ō-Cegŗ ĒŽ¤ĒąÎ"Ë lĪB1ƒ§133S”ēšBĄˆņ\ŽAېÉį0ü…ÁĀŲÁlú‚…>N9“edړûßW›Ą$oģFg´Đ¤…Ä‚éwŽ8=ŗJ–°HšE†œÖääįfÄ>đŗÛV7ĖaĀ8ŧŸ´E›ˇ]Ą5X(šn´đm4e(×bąš"3„˜5*5  ĩEy9ÅŲEąšōejÎPÆbŊ„LurH_Į͚Ė*Ž?ž3WšĒL>!Wr›žHšfékyË ÕIü'´ŒC,ådbåÆŧrf܁0'÷"đíŽM›™i“į&†āô´Ā‰=pJŠã6CąRžŦ .Î1īsĢ!žÁ “Ęà Ļ(>Ŗ0W*W$lį*û#‰{:Æ s,.đ„;¯Ē‘’pđĄ?MũŖ‹afæīԌO>A€ŒøīgDJ¯JRčc2UŽ1¤S”uę ŽPFÍöŠ”āc* Ž+Č7ĖÍ:&gj‹ōōŠōōĩÂd‡†)ĮYõ'ž Wō4R§ÚŊĩJo´Ņt3ÉļPæ/Š‹ŪũaŖ ęÔíÆ¸Ü-aL0kŠ/4[rÖČr(øØ÷^#PđöČܸpųd;{ō˙$ž4öÖĩ-˙]ĶĐ÷{ĀlŽĪÅĢíg/éö|Á(€†´ôļ›ģ:éÕM5’žžžcč‚6Kķ-žī(°M=žįcŖFy:2$Ā ›Fqŧ:ģf;Lp’lÅc`ĄoŋqhPĮ{}d“#Äb Úe:†ƒtŲÚŪ=Ņ×Ŗ÷†‰O_Üŗø].ŲZ{ĩĩöė?ËІŸęĸĻ:ģ(pŅ%wpŊ›ķnvßÃīŨÉģƒžŧ9(kkmĀ9ŖúLmŋeˇlá?>.ÕčéŌLŨž]@v´wƒī@Īō}iז—‡s#w"77€Ū!w4õ?ŖŧûéîÃÅۜ_wĶĀÖÜãÛx +8/?'aEylœĶĻ‘K œš*¯œšŧ,ŒĮ^rĪ"VÔÅ)īv į1ūÛķëlāÉ(]~fpc:Õ)ŖļDĨRŸ0ōbļäÆÅXĸÍ=ĶÕęxĪg@Ã7U; ĩiĒË͞‚ŲŽOް_ÕVx:moM-øŧ8đF;ßÉ!ĐؘĻ:{¸ō“†^€ŅŅQl0×+3žßųŨ•Z3ÕˇģÔ×wL›“‹>Ŗ¸K#¤ŠB›s¸25ßx`ĖTAč(wŸ ŖĀŪpvįĄŸŌö]lēoŽÔx3ņyĐŲ¨Ė9ą%į‡ž=“Œ¯¯'˜ĪžĪ-šđŊĄ7”˜Č¸ŲP{@ķĪĪ4–j.æžX{ËŨÅõ_ëČÃy"™2ˇøXē”­ÍY!–&g?ié[ę,Øę´FÚåŧ…bT8ŽV´é+ufĀØ¸­šyH NX [_ 15ų:vL؈ÖišĸĢ‚Å*=/VČ!-6<„ōpŗĄÎâŦŽą(uģ1.ķÎc×?F„i˛ §†QF­F÷o31ƒcBŒyy}o{™õĖ:MLHåÉĘd7HĀp؜÷æ8•cb¤CķĻ=Ęf ™a1Ķ88€ĨÎ`sĮ˜Đlą936ÄXTn (}y‘1$6˜ æ2Ĩō‚ô@áFf^rN˙˜`.ËTQŌ‡Ûø3ĖaŒNúIY,Ÿ+â3l…œ3ÍVŽvŧüb3hĩf&úŪĸšõFđcã`Ž,ĢtĖh)}qA[Ø˙žą$ŗ˜ŸxgÎęŧ:€ą,3A˛ IĨ‘g+ĘN–ęÜ[ŖÎÂôgc8› FƒĀ\§k6”đ8šŋfëîæšņÃwÛ9įĮiM‰ÎFMž+ä8Ļ_EŲęaį„RzuԊlR~ K~gû§ĢœÉĄ¯ėë€M_œ¯g ÃØŽīåÂ7lē"U˙ Ē6]™FoĻ€2”Ģ´&bĖŠh›ãÕ#sĨĒ„Jø8€šR­*á1}û€Œø<qØŖąX!SÖI˛ ŠÜŒe*U™ŅuceŋČFĒŠ &x8°ųLˉ*Įq’fƒÁæĖÃ](ĮYõ'Ä0%ۚ)vdd€jÖ[œĒÚ\ĻTTŠŌĶû-Į ‹åé˛ķ¯ĩQfĻŌ D@ķD[÷ _Qúžãäûôp6ŽŪu>ÆīÂG—ęp›>›ãå˜2ņ§+{(Ŋr^ol7΋‹˙0uÆ(đ_Á)š|ŧÁĸmĀˆ1W{ø,\2ĩųp­ļÅtü{:Į Ž9ō6îŧ×#SG=XqãüEÜņ Š‚wc'Ū+Pōčnŧl2÷˜ņV,™č18e4Ę˙?ß ?ôŋú“×L'ưĮŠ_Ÿžd‚;c…Ž˙¤ŋÚ ˙ ßŧûR Gķđ‹<pŅ%úŅ4īŲŨu^OŪ7˙ĐÄ?Œķđ _|áô‘FĒÁ@†Nfû´ßhŋk¸RŖk3šô§7;ū§ÔØÔŪŅĖö pokę/Æ#&ģÜŖĻūÖpū¤Ĩí¤ļÃĖôģˆéĘåŖ•ūSÛh: îxĸ``žÎ¯˙ēÁų"™R$Ŗ •šagĮãaōÄā¤5‹*“Įģ—Gp$iĘēT…˜ qؑë˛Da1ËEE 1ÁäĮdė÷—ãĮe&+ˆL p‘œĮÉJžå„bŅ ›Âų13d<€Øäš)ÛeR&›ÉdÃVšŠĘíÛĩ†!aA>`üåŲš˛ÄėÛ•)• ˜!’iÄČÕđddZ”éK %€ŗ…Ë3#§Y*3S”uÎÄÁq )p˙x9gŒ(b‡%įf°ižYđŠ<8!á5‚,ĪÍæßC{÷™īāaņëÂRRI™l&›ŲoBĻ(^’Ÿŧ€Čd‹ŌÕéVß9â-ĢRÄ* p^ĖÆ 1(Ŋ*e7)? áÁeÕŌĨđëL‚Céōrę„ĶG’Ž2—)ÖäčŒÍRąHʏIĪJ æ0ĘáũäÉŌ…LšĪf2™8Ũ×seĻT‘žˆØ 80ĻÅgfJ7fĮ+•K%™ āĘ )ĀlĢĘK؞bŖi`ĮlĖNt¤)l•™y–˜›špĄŗę€O‹ĪM>ôÜĘ\œ°hģŽfp믌0&ÆķRVHŠLŸélķoŽ”Ÿi‹‘9KW8­ÛB¤*$„ €-JĘttÜĻËË,bgĮuyUÍ$ļ}‘fģã4.÷@RˆĶ(}öEšĄŅbŅ&HJ’ŗŌ UykRRl@L$e%‡á.īāÜ7,ZUN‘´īUۅ‚íÛš,02>{Ŗc~|§ĸ y ’‹b‡É3Ō…LĒš)æq˧x/GôŖIÄģ8Sņšœ:ų +y„ŊbąXčŅđ ã<ķÆbąęëëĶoëëë‘[ œbŋuŗVßepķĪ??ė9Ü†ĄH™=ôHjœ/Ũ˜$b"å žāE~^s¤ōQOŸkÖÔņåYOg"ū4ī…ŒˆŒˆ@ ~E8Īŧ™L&N×ÚÚÚÛÛ;¤‚ģģ읝īôéĶĮ˙⚍  12"2"ųŠõ ĨXžm\fŪ"""ēģŸ čîîÎ` | @ @ü̃7 Ã0 CÚA @ ˆ_ nH@ ‚7@ @ ā @ @ Pđ†@ @ ˆ_ }–´ļļ>–æW;ÄŗbdDdD$ōSĀĪĪšâsŧŨöô},>ü˜ÚA žŠßv4.4ÄȈȈHä§@ÛõĸŋķölƒļM"@  ہ@ ‚7@ @Á@ @ Pđ†@ @ ŧ!@ Äŗ„‡Ķ̎{iũÅ˙kkĩöôô ųĘŨŨŨ×Ī/dęË/¸ayĪÎÆ˙ŪĢģøÜeŅŌqîŋNŊt\øqSYø…m{7Đš‰Sēۏ8ų]Œ#ĸ7 Į¸*eŋrúŋūÜŌ=fÂGī‡OänöKEe{ ĀzųˇĘ×|:@ ˆ‡ Ū._¨qw{aöėŲNŋ­ĢĢģ|Ą&túė‡ŧ§ûhAÛģg4wGøwöO>lhŒcĄüíc„j:–šēûD øŋĩī/ĢøÃĩBž-ŌĐscgq€ŦNXŧ;lĪáĀg\uFZ;i‘$ĒvĮ› zבÍaŗČdÕ­`Į,c€ĩxÕ’Ãą‡ŋŽ}l5‡j(=z%<Žā˙Ę|j÷úŦãu$0ƒĸ7(×ÎīŲ¤ZõŪgŋĐwūEė<ž> xNVhaĘŽŌF;cîŽŖ30 › Ö|¨]}8cö˜•N7ēã‹ķ$4ŒZ§HąšŽĨ¤Ģ´WHŌėÉ 6( ­ÛõÎĒoMw;í7˙˃ÂĀYõĮYŗi՞KÍ­Öi[Jrį0œMʒüˇpūí@}HíūM[K.ŲcG$fŦŸĪĮîaqõúôB‰$ &ĶÖ: ģlÁvúãĨû§ėŨģˆ×įĮ–.Ų]×ßcú†ŋdĖįĀø>t¤ėHŨ_Qg%i`ņģÖÎįa#Ēø”AF|Œčø™Đíy/6ßyĸJ6ĨzŨ§›…\Œj(\ĩfS~ÄĐĩEW°IžŲ;(čĨōV)Ē_V|^:“‹ƒ#pĩ–gė¸ Úwl!čĻ‚5¤V† XN{âŦ:ņoŧuuuŅôŊŧøÖ­[#iŨnÔe•]ĩÁč°é\VĪÍkW,õ×í0vPKÚo~Ė7ô2',Āų“Mē“?į{G¯žÜ÷õõšË×Fߊ0āJwK§Û‹yãŊŨ:šMē-Åߜ寚ų’™\”tõí=‘ā[ž3tΞ3՛5ÖÛÚî#jĢÉ0Ęo]WŒõúKģ;=•‹'ö)Ĩ§MwŨ÷E?7[+U{˛ē÷Œõ}ŗĶf¸t¤žˇfō(‹ļj§ĻŧĮžN°ģ.֝Ōë˛=ÛæŽ9Zõįæ^đfŋėīvUCw˛*ˆ˙rAŨĮF}1įÉĒ}įģ<^œāįMĩZ‡ŽZŪøîÎÂnv7ž?û÷Šķ~/øÍ˗/×܏ąŧ9GŸ€?Ÿ#ļę4ę/Ž7LŒßĢ Ājw§ž°6{núceĀrĨŦ*E{đķ]ŊfąjŪūĪ%ŒŗŠÅ›,3¤‰´ĩ’ūomËZ"ĀlįÔŠYjãĢ×-llĄW-Ģ`úE§eDŦĖ ût_(Ķņé9š`̎m ¸ĐķĮ”ŗÁĄXŗ•´´2æ)v&Ī` ^oÎųcĘY~f!ī jw/U”ZĀø1v­žÅĄĪ*–í‘ QSQĮzw§č§˙ŅXHSsį”͇?—0LåŸ~œsŪ 4…ĘŌ‹BĀš,Ξsę­YĮęh 5wÕļsXļZujFa /Ūļ+TũņyŌRûÎü=XđĘĪwŠ˙¨„]ûųWîhĩ´˛æ.ˇiĒ;I LYˇks †ÉnŲŋŖ¤ĩ [ģ0 X’ļŸÕˇ´?\{8}?Ȋ”Õ;ĒH˜Ą˛4…,Œ>ūņžz˛sĶ›‹1Æô Rg1īuĨuãįo  „XĀ;RZGF‘%ŦøvÖŪ=ņ!Šũôƒ­dü—ŠQœŲ€ŨįÃ×­  jwô{΄ßŋ|ķĪWŦūŪ‚ņŅģ˛Cæl80ĮT°ęí`}p˜[". í'ƒÖī˙pmI—XšmK €ÔŨũņ‘j# 8+üũ-Û"ĻI“…†1ũ0܊pgõĨIĢ…fL›Á¤úėgMķ”ŅŖĶę´ĩ*kĶZRģDž„pÃ0ŗ¯yįIJŖ31Įįo—͚y'ĪĄ^´öRâÁmLĢ>Đ.û*c`&Į‰8t]ųYL´s¸1o rŽœ5/t•H< :Ļ/á"ˆ ÂN´ ƒĶ°yúWqį6ŊšįQežã}ɜeī}ãĢcK€3ŖĪ…l¤•ÄsĮcŽ+Ėq-Ü áÁ´ÕŌj… ø Åā<Րą@, n9^c"-­ØÜ”O6Îd #>§FäÎĪØ­ßķÎú×xsĸ I|bx0Cmé¤õ˙žîH¤ŨŒZüyōL‡uī”ĄmWL6ā21Ļ'æĸ'ŽĒ?Ō2ĸ3%›ŠäÔŌŦđDÅfiF5äüQŅ9ã÷=íã:jūlėĒoŧfõūļš`´D9üŠMem×´ŋ\}y’ᛚSíŽwŨG ÂVĪhŊˇē;N]W˙K8ÆnŦŲT`ėôāã­[õ2,‡öVUSŨW;zŊ'ÎwšÆŪƒ¤Īã;o”õRųUžÆÄ-ZžvXÄÅB×mU|&Ú÷u čę*kČėyrįpÁzléƜǨOúxО48ũpV(Ŧ—4W¸DĘĸĸT2m˙ē €>[Đ?Á)Pî6.Ü÷C LĨ Ģ?*˜ôUkîĒm’ĖvęŖ{ŽĮí]Āžh˛-† ļs;ū˜‘ģ]ČDŲۃë8 ōė֕;ŋq8‘ @[›ąč {ÖņY &öÎk+ĀTŦøāčôu1,ZŋS>kŨ×§0Ôîzī㒨¯cÉâôId=ž5ëRLÚá .€é؊ĩģ+gŦkūTMŊ{¸th“Žķ]ˇyzõą_ @5 Ŧß' ņč;o!?ŨŗOÄĸuģ–¤ūĩ)fe>\–•b5;‚?ũ*Ž ¤ÆĩöäÜû‰Ā_÷éQ °–+ŪŲĄ‰ū:6zķjõ+ŋQ†‰TŠ+qûb9 Ŗ­Í4&Z˛sãųwÖž%*ŨZ•–5`ri-ßSĘ\öšˆ1ÄsęoÖ|ĖLûĘåFYë‡qÚO.˙­ÍYÉeP yKy:b3˙ÜîĩG°Í{JD,°5Tč0āÅn[WųŪÛo•ž<“KZgĨĨߊŽT.\[IÎÛļwđîOÃŅœĒ‰ņ‡‚0Õ1<ã°´ŠĸčČîw÷ĶÁ„ėũÅŅ!OnŠ0y:_c"bšTCaAgTš“IęqHK ‰1Âb ´6‘÷3­ĮŸcĖ]Æ Ŧ…ÎęĩoÅ`ĀāĪ\kČC: IDAT´.E&ŧŋ6øK¸8ž4ĀôĨí+?8Úš›ōI,÷ÁĪX)`‚ŠhÍ{Û5ŗrcXÃ~š6įÎaQ 9oŽ>ųéį_‡2l§>Z°˙øō™Ã~U‘lįÔ' |ķDl„č_vŧųÆn`p…DüæÕQÚtĸØŊôoMFã‹dV/a°bR6hÖ~]9en(4OÜđŠĢ]ĩÎĢ?š†+ybš ĀXōŪŌ=Õ1Q&zæēŊĢlƒ‰E/k…bm;eLEĘ™YŒņáÁ,ĀØA,ēÖJAN;“edړsī+i(Wí¨ŧdlĨI+‰=ÄļVŒb[Õę˙X9kķžø¯đŲÎå}Öĩ+†ûĀφaĻáũ¤[ĪæeĢšbĨhēą5ĀFĶÍY,æĮkiĖ (€ų”瀖š?Š>,oiNÉüŊ}šGL¤,9M›ŠĶ?Xĩ‡ûõ7LŦģ˙J/ŲÕˇÔ|ÚUu`DÉ×GÉÉúĸŒŪ^¨Zžo_bĀz…E,žõŲŪc†™ņāqæ[Ÿ;{Orˆ8\Ā´;äŲĖõ9ļş$aô´Āš˙eÉ ›Š3>HÉSÎbŪgN<Ä7aą˛°AMŲxPŗŅZŊuí‡;ÆĩĨũūžN…ą';¸Bë‹ĶMTĖāœ*Æą÷°Ŋš‚ƒ€qšLģÉöÄS‘˙ũŒH5¨×¤×ĪS:2‡`ÎÜYQŽá”ŠbĮڏ×>@6šüƆ-s¸8ŨT¤ø`í~Á7kęūĒníüf1Cû7Õ%Ēüsá.2TNĢOyä}@Õ<…ŧŦŪQ]×BŌ´‰ôŗRƝĩ„ŋ; › Ōw7ŋõɖPX+J.›,û?Zē€"oĨu˙į5oŒŊgŒäņë×-†fKMÍ?u§ĒŽKŪ}ŅËĶ‹Áč‚Ņsß›îå¸ŪcÆč4nš\[Ũā7yËģö­k™Ÿũ_c_ūJ°)Y0¨¤ÖEÉ>zģ(/§7鱏´¤C#;ēãn1÷—¤˙_Öā;^Zg¸ŲŨbĩĪn’øx/wčpy‡oūy˛ÆÜ„ĀßF,đīŋÜí6ŪÛÂč‚[×;aŠtĩßė|´ˇ‡ßâÕą‹ļÕŪ1y2=zú;éˇÁŸŨzŸËq‡‡Žû2MđőŧUkŽĮ.”-‰™âją–zb›đą‡øÅ§i ÃŦÅĘMEA˛öDņ0SÁĒtÎƚOíX_9%íĶži: ŦȔĪĶ˜AŸém°IōŦ!KŒkŋúKmuÕšŗ'öŋˇûo›˙ĸ ŋŋČNžr#’åĄĩg8üQjeT†rŸú=i˜WŒga´ÕĀŌJc,l@Īi’¤>¨›ŠöT¯ü*ėq<šô“Ŧۚ’‡­ũd/Ā$+V,+tŅV<Î\r4ŒËnbÖxëüôÂē…îv ãwĮáz8B2Zw$§núē´€T§MÚruū_+ŒãŖ7īZķP‘ ÷Ôx?˞yôGyVs­āũ×Áđ]qØãļ+}ë4I‚WĀ}Öë­Û×ī6Ėû¤c-ö -`}#€xC°co“ î=īąo°Âcƒč­gŦĐ7ĩqÅNzÄŋ Đ4<Âņ0ĸĶĐtMzõ4åįŽ6ū9+€9Â*œ;ka#ĩÖJÅ0Ø Y,°€š1ģKL4M~QNĮî™ÅãoåÎ`zÉģG.%Îpvp æŦ:Lyœ/qtŌ€aTmÎĒOMī§īLeQ.÷Ķ’ēũ› ëž\āčí5e]Æ ­ƒw” žcžė9OoÚ[õįķׯõ§‰˜cĮ :Qć7o‚@׉ī.wŽņgmũąokōĢɑ4îéáĐaüûüírãC”Äãqčúû˙V}Qtjūn2ɑƒÖ†¯KĪ;×nw]ōÉĮ×ŪÄT/h,ĢÚSR“uäōu ‚—}F\]č 'uĮĒ>÷Ëw?œŨ÷Ũ•/Ū<č<ĒŽR•üœUÖ/ 'MΏÄFŒWøĐXĻŲĻū1íĪÆŽtÍÛÛŦu‡žŊXqÅū|<Œ7sŅ–ŒÃß(°ĪåŊģø”ũÕæĄeū˜ĩŽ•[ÃYŖëˇP™AŅÁ-…‡ûéÉúĘZ+`,œ45ŠÁš2U_Pi0UäײæMŅ™tß°´Ą\]7~žEZ:ą`Q8°6Õĩ:Ģc:–šeŠÛ’Øŋ5ˆ;/;ąŋPOPĻŗšZëȕЊnQ}qĘQÅĒ?UaĢîT=%]’¸eu~Ĩ‰ g€­Áú€†p* ÆĀH‡æM{´í É žÆÂŦuWHG‹8Æ“ÕæDĖųÁ-ĮʛhZ¯9f šĖ€ž—īßøü/ky[ķtũĪŦÉɡ/X÷P‡3s““~ŌV LەK0>1ÅĻ)Ŧ$lMgĩVo"fМ5ĐÍ &?…ųôņĘ&€Ž/9ZĪžŅ”œŠtw9ëũ•wæŦÎĢ˜Ę÷|¸`ŲGų ‚åi‡ Ķ%3ęܲéB+ƒĮÂ06ŒWŦÖú -Æ&ˆ[Ė=‘‘Ķ,’G›I:ãáteŠŽ“æ¯õŧ˜pŽcúU’WpjØ+ŋ …kÖæŲžĩäΊzŽZp6‹m¨îëY_r¤ž9} ÛõŊ\øŠ+QՒ}Ÿ5z+ TĶņüs0-‚52§ĸIG^ÚZ_Nŋ€˜OĒĘëGļ›b`Ÿ'#냹üãĨõąéƒ#7ĶqÕáãF׌§ûE6U䟡ƒE\bEXՑ #€ŠJĶä?CĀV ÃĒ9cĸŦWH—ˇ8­ūč S2iĸXŗ"CYtsƒÕŠĒ͚ŠgfĨ)ú7ÆŗĻĎŋ”sđŦ €6×VTZčŸt?ŅÖGûs_sõ›j-˜ŧø5?ĪnĶ€"c^‘žęņƒîīzK­žF{û†…0ÚĐUąéZį¸×gp]•|ō¸52iÔšŖÕ7jõāކ-~}ĸ÷Hw{ēŸų~ņhĩŠŪ`lÜK0uœˇĮ˜ įØŋĶũŊŪRŖŋs†¤Ķ°‘ØČķĨ×gJ¨ŗ7t]oĻũqk3îø=3¨îfM™uEWŨÚV}žÃ:î7Q=Ÿŋˆˊ[?+ŽnĒŦ4 ›‹LYžR°FņÎ"ÆĪŊWŽ5?-Ĩ>5}ák4†+rõNQhôōˆcŠ‹c™ŖweDßyPÅmIlL˙āĩ=4`\bõ‘ŦäY+RWVØHŸŊQšˆģ*j}Ö{‹ŧXLhđöčĒĖÚ]ĶĘh\ŋ$°€åiŸÄ­ÜšáĶ뗩mĀÄŽŲLŒ\MŦųJëÖŦ÷^Ë 0ö YÆĖ)–39ë3ę)Œ3KÖ,āæŋLÆVŧ÷Z +lõ'ģ‚FØt€Y0tą`UJėk Áō];ų÷ĐŪ}ōĸXØ˛ÄiĘM\É`{ą˜4@3B{xĶ›oå°#6=āĀ`EoY[Ŋ~ũÂ|đņҕҠõ‡7í&e_.đ`ÖĶī­Ī?¤ŒâЗžØ_/\ŗy${n͚×îŋdlą’éī,š–2Ėaļ ī'+j]lięęwōũ æ”9sCÆo_û1`˜ōū–mŌĩ;ã3vŧŊ,‡ãŖļ(đĖÕ_ŦßŊž¤i`ë˛V:Ōdå^•…Ø6āåLčŦ:`ĄōŊ+ÃyģĖo.˙đŸ^ĸŗnW(€ WĘyĘüÁô ` ŸÉņD ø{Čyo9[tw&>3qŗčãÔeą6vD|†C*ōŌ{ŽąĶåC†CŨQUU ‰}úNô§ØÄE{ŗâCœļ@×į(>.ŋŌdi=ģjYé´U;Ķį°€Ŧū"}ĶzhĀ‚EņŸŽž‚ģŧ€sß°jä}#JĘ[máŽO?ļĐ4`‘ËvntĖīīT´á⇠ö[m4+lņļ´ ēYŖúŦ!16f$ÔdPŸ#:ōÖ¤ĒëL& |ôĮ3‚åi;Ĩ\SųÁãÍ-Øî5ąģ€M|ō—õSāJég_BđÂh洀Āvžp}zŋȋwn™É€•;ßßĩéŨˇvÆđX—ĩ,0HTÆoŨõá›G0ĐˈuË\ôsRũqoC”ŒĶ‰‰7­ZVĘôc°V'Ī úŌgŸV4ĶŦõˎæŊ++1.}ŗ1}Į‚ˇH`NŒJTDĄ˜ŅĮ ˇoßÖjĩŋ yyāÕo˙öįYŗfŨŖZuuõŧ7ūcČÅļë˙ô÷¤ĶįûõZÕāåéŲMĩ\ĩ\Ŗ€9}ŽržŸįs¯™ķqA5ä,M‡´=‰!hc=âŲbæōŪ._t(#úQˇ¤jwŧ™ÁÚģ?ž˙æSŧ8ūĸZÁ[‡sį0‘ŸÉ‡Ũcų +y„ŊbąX€xvqžyķõcÕ×× §ßÖ××ûø"ˇ@ ĄˇÃjiŧÚ ā۟eEZA<MEģōĒ:_ķ X¸6^„ÎCF<9čúüƒĻȔ¨Güŗ͕õ‹w>‰øĶŧ2"2"øá<ķÖjnų‡^×ŅŪÚÛ;ô< wwwo_AH˜{üũW ˆį4.4ÄȈȈHä§Ö+”y{ļqžyķōö ž"ėîq~,‡ģ›ģ­ƒ#@ Äŋ:xÃ0 ÃĐ.@ ņkÁ Ё@ @ Pđ†@ @ ŧ!@  ہ@ ņ+ĄīĀ’ėmĨšĮÕņ,Æ†‰üđķķCŽø\oÅŌ­­­Čc4.4ÄȈȈHä ŋüō ú;oĪ6hÛ$@ @ ā @ @ (xC @ ŧ!@ o@ @Á@ @Q:Ĩ˜PčRƒfēHgëû\,ÉŠĖĶÄúbu™ŅōĶåbB$"ÄōtË{T2QÄDI•Ô`Ī1”)Ĩĸˆˆ"YK9ŧJ-÷}~ÜūZŦ”K$RŠD"MČėķ9CQ˛LLˆD"ą1ßdw÷ũíäŪķ;ÚŸūģđ…žĻ#Ĩĩā45uÉ$ŪP•÷4–~¯<× @‘ĩ°âŖÛ+ˆėš6Ã?”ÖŸoéļƒ›Ī‹ŧˇMļÕü雋.qĢÆ‡įŽšč5,V4ž9›ķą‰‚1lXț0:ö?__ÂčļU•ęŽœĩ˜{p†0*,>ŠãˇZŋ/9ôrGû ëÍķ§ Ô™Rím[;¸qÆ{š[(ĀyĘaĀ-ō|ƒÅî5ÚŨíŇN_íēŲn>ėqķcÃ%|Īįz Rf}3ÂgU——WGø!9Œų(íY*Õ*Û´X`Šr+‹ŸÚ GUE Ĩ>x˜˛LķĐÖhÖä̃ i8uĨäņv´Ž Oӊyø°šjĒō‘[Ļ ŊjEÂÖ˛Âl1ĮI|šēR~wЏ('˜‡Ĩģë9†liUHFi‘ˆf2%Sķ˙ˇwļÁM\iž˙/ļÕÆQƒ“V*¸CbMÂHĄ°XöŠÁÚlčdSj>¸š[#WQÔEžÚĩļj-îŨČ3UV>ĐT]¤ÚY:39UcUîXž—ȤĘJíDŪ­Ø5Ü!kÃ,šÍ ("—=‘ŗĐ"A]Ø´0tÛ8÷ƒlđ‹ &ŧĖ$œß š>§Ÿį<įí9Ī9GÃ9Ŋūa8Ėņ€”ÛۛāMĐŌRŖ/Tß/9(CĐŪ´ąä„׊4ÔŦ611(Î)šđîîŗ×PcĨ’#ŸÉÂj6Ũ`&^Šķų„{gßũ+YJ= ԝvƒßįÄí17ģlTMƒøî!›‰RĶ’ÛčĢšLjÉ´l´ÍߓSjęä`Ŧ?ÜvPw•ĩdŨXĸ‰ķuúí,|2°[”Î:%ûŠ>bˆŋF„œđˇERÃ9ĒižđŒĶß´ŗ”–‰y›ÛĸŽ„ĪŧPģR/”z˜ī? žŨK¸YhŲXŗ'´GųBÄsGû]fJî÷7ˇI–Ū€*) đ¤tlįúõëēŽkšļܟ7nܸ—¯LËãÔÆ-ëž/›Éžûé¯&L´)ã“Ûū´öĩMŒéæõķŠO:NOŪJ0uqäŊLa3 ķ™-<ąnM016ŌyzÁĒOEõzūÅr(ĢūËīoxÍô•üņĐÁc—>׌öMėÆJmôÜ9ņčŽßžč]~īÔåÜMLŨvŽÆS—VąĖ*@;s딨§ĀdŦ&FĪu}ē´ÜĘÖžômŀÃōņ •+ŌAŋtVėū4õEĨũ/6íÚDM\ŧđvô|Ū8'<ÅŧöũīėÚļfŠKt-û‰ø/ōįÖ>ķôFã<ąqcččGo§”Šõĩ{øÚĶ…ÔąSoŸŊL~td°ëÜÕ #ŗmĶĶ5Z!ulčāĮj)cŨûÕāĪRã(ž–aqsŲ§äBY­™}ÍžŪūĖĒ åŌ{G?9?ũ˜ļ@-ŸNH>×ÎæHZ´t(0 äâm‚76<čã<ũłÖ}œ'‘´T€ãŧ~¯ÛíxÁ]ATSQŸ‹ã8ŽãŨb˙ÄĞąÜûŪŧā–ŌWS~vX“ûEĪņ<ĮģÅ~Y´Œ$pnŋĪãvģxŪ#ĨÔ%ëÍ’Āš}Ū/h頋sŋį)ÆK´T€Aŋ›ÛēÕõίÃĄˇuëÖĸärŋčAāy—?–Ņ–ÕĨ´Ã“Šú]‚ ŧ0“RĶQŸ‹įyžão4s5:*œ=ĐČķ‚/‘×2’ Hmž‚Ī{‚ąpĀãvģŪčĪŖ”.ZĻCėSÆ:šy^đFŗˇ—ö—–ŪŨUP“~a6wÁMĢ@ž˙€4\8îßÉķŽĀ ēĀŠë˰ N+PV§‹M÷ ĢZ6ęæ\áŒ@M]B`A@NËDĨ”ŊÕežWsūög˙3Đ3Ļ$쎔Ņ`âÄh"ÖjĄ•įŧ “/!'´LØÃssī$Š!5 ¸yŽã8ŪåghēšS4Í(ŠÖÁŲX €Ē(]ggĖ ĩTGDvžá4a™äš2jæŨūpæ…âō ០BänA@"uqūšāsķūE‘œęhŲDĘĀšęh€bMVy uQ)3ĪÛLÚRo1¨rËå@™=áøáíĖ}OēoՍY%…­B4;ëŲ9;KP Šj°:j¨åÎĢĘ^įõy=n—ĀģüKâT‹Ú‚“üÅNÃT‰_#˛‚ëílĒ™ԃåx;K Ėv ­+ęâŽŗä ˤŌÕlN@ihŠ‚šI娆íf ËyęõĄŦļŒ$cąT!ËqßloĘšüÅX –‘Ū xø­[yqā˙ø\Å1“wlŨęŠf䓒×%Ī ŪpJ]ÁxMxŧ#oSSSšv§°ōÔÔÔŊ|eĩĐhßQËÕŋū‡c…üȗ—ąö魎ziâü…ĢōøęŠŧrâ ä/Lę˜ë~žŨt°‘5U–UTN¸ísicEŸŠœb×1ĩÛ66rõؑß/ēyĢžxiķ¸ģî6\Ŋįo^ŨašúŨ‘É_¸ŽcŪiēęaËHßč8Ē×ũ`ĮwŸžžō‹“/ū×úØR‰üšŊũinlääøs¯ÍvéO˙ŊgķFcEEeEÅĀëßüģ?Ģžxđ{Ôk{^ųáú™Ķī|øŗ‹¸|ACŨÔb_á¯Ö Ÿœ˜Yk~a×ëO09r÷"Ŋ9šËĻīoiæÖTÜX}ūĶĶg&.}„ēuÃ}ŖWad°Ã\ \ËūĮÁžņYk•¯ölŠúøŌ$PQģå įšęéÜÁƒ§Ī˙w\~t`öüõ&ģqfcáË7˙íú™ĶWŽ­/$.ÎĢwíq&Œo8ŅúËņĪO~6ļmĶbc•Žô^đ<ŋ=¸­Jŋjũßōd‰Jgōļžrų—#ųë•Úų/.Ojš€ÕUĶĶōéîHt@6mwī ā(ސÚÚÛˇ';¸wc‚ ÚāĐ2ŗFØßÉąČĮŨo š:9­ŋ͟°„a|:9VÉMqŸė X)@KõĖMpzÚBšĻXR`!'ŧn_5Ū į?$˜)5é¤Ļ˜‹]ēäŧ_âY¨)ąQ 7ôŦfOg"`ĸQ|ÔãJøjM‘)g{ŦŨĖĐ4õ7¯úČ _sÜŪî4i™p[7ĶKØh¨)Ņ} ÎÅ\Ĩt)pĖ÷‹Á´3œX@Ž{šCƒöö\(Ēûũ MNÖØÚ÷Û#?čō4-3?ũŦ‚rĖĩŗ[=ünĖaŌŌâÎ@O–÷™ŠĨ紊 ĸĨ+îf¨ÉåKĪSs7hģŋ̟5Q@žß×pÆ\ü~wcŌ˙d§üŨĀÎĻĻƒŽä4Ęá9ܞjl ŲÃ\Ÿx– vqķRųˇúč–.Ŋ¨æd&Očp|Ņböüō\XaJĘɚ›‚Ŋ>–´LØŊ¯#í šSĄ}Ũ†ũī&&¨™dÚÖ%ųģwŋš°×ŗĒâ†o)Ĩ úų}Į ĩ ‡:†ŗ=Ԑšõ]+,“œ˛z:Žl2 5wčįŪoĨZS47xŦŅč€ĖšX-ĶĶŖnÖ/ũØ"uT%§,tQYm„’-wæzR4×Rč÷–CahßĢhs}“_ôØé|éVŨÆîiĨ-Ė<…ŌÁÆæ÷sĖöũ ė.€Šk=äŗŌãwh žS0-éšvr&-#íÜ¯īŠÆl´šô ‡‹}¨ũ)1â7͈jĒû8ėûÍÔ=Ŋ0ī!ÅīßŧŲûęņēíușÅà ĩĐrßņŦĶeĻ *:TĨđˆĮ÷%…Ėloīm`i ãn÷[CÎNÎ@•õú@į†ĻiĘųß䓁æ0#ēːãma­Ĩ3á0A˄Ũmag¯%ã5áņuŪUĻr¸q}rúĻ|zPėŸ*žXŊvI8ĒĸŌXS]i0?2Tžf2]Á’IDATĮËĖÉ~erėŗŽąĪTŊ¸Il|rėŌÕΝÍ%ŧ4P+Ķ{Uuu á.1ŸųkĘYSÕ@7äÂ\XМbMUÕå /øDEUpsö[•Ģ€™)@Ÿž^"đ×`ęōø €üŠŊ§n?œ(, vŽŽŪîˇnNLOĄ0`íúęEž’^˜˜åí˙õ¯ķ´×& … ¨zžĒĒĢĒ0>ycr¨^dŦéŠü åĩëīh‚BŽķĶ'&”¯][6ųXÆÛŌâÎæ>CÃÁđŧų^Wg)Æna€6›)UVĄfû2Ŧû`qŖĨÉÆ™€lə@z@ļļplqaŗÉ*EŌų& ŒŲRC0Ԙi5Ģj‹›ÁȚ ëœĩãŦĢ!7 ¤dE×UEu¨jĘ,4ņ6vŪâo¸­ÃđÆģ+…ėPrXÖCŪĸ?ĒꚡnYŦ JÍ6´ážĄąál›ģ4ÕĀ*mŠŖ%ŠMLoˇÛœƒ–]ƒŧĨ m6kėŠą0úYEƒ™ŌJ鲲ŌķÔÜU5Û %͞ĸ”‚Áĸß{e1ņAiČímLÖīmÔüLG‡ŧũPÃ=Ũęđ’ yŠœē’Š„ŠŦĸéú˜bVu-;24t:L@[9OFcZKW/§§â‘ŽD8ŪĐéąR@9¤äiMNšŊRMĖo<Ÿ”z´Ļ9.?´\rĐfÎ#r5÷í~5˛ˇ7æ3?¤-`&n¯ŖCŠg}ãâūüíčc×î([{×aK¤ģÃÛ<ĐĐäilË-ÖęZiæ /_cīˆŽë€ÁO´ųã1ãXJŽš›Ī–tv’ڒuÁčė4]ãģ‚ķŖMÚJ¯=ŌƒĨ%ŧh‰1ĐۛJĨ’n)ė•ė+h…Ku^‘._ģô˛Q_ ÉI‡bv2Rc`žō%jE cЕYīOUtsk'”čĒĒ΍ŗq)iiíĩ=ˆ‘ģ„œę čí Ú;;y3­&=BOI#EčŊũ6–ë?Ė*| g¸Iŧ-ÅrΚP÷°ŠĸKĻĨģĨa{{Đŧ‚äšœę‹FcĮeÖšŋŗÉųĩ<7}™/‚ļīuęû"Œ|ÖŌ"ŪáÔĶ-uĖL ­fg‡]-€6ßez˜OŊĄ1Ą3<ëî%‡Ųũ¤ ÍœË’˛*î<ī_qŨ0Ų‹&*˜÷¯8ĄĒ¯¸Ą ë¸)&1âˇĀˆĨ\Ͱ/0d“ĸūeB%_XüPKGZCĖÁ˛`}a‹.4GĶ­vmu‰Q@ŽšÜāūpg˙Tƒ––ŧĄ\KøpĐfŌRūRéWĶūc{—{ļŗĶ6į‚­ w”cÍ}†×õĨ:Ž|$Ļ4`Õæ—×UcUU9€éķ'Īũđ“ÎOVvķĪô•#?øņŅᥠ…šĀÕęZãR'ĄrC5|Ūwę'GÅ^åūÜÛ5;ļŒūrč'ŊЃŅOs@Eí†mā=~ešįLĀäčųŽägû0yŽãčšķ7Pe¤*(#‘>sôôĒąŽĸv SLeNũøį'?›Û3  úYĄvpŊŋī|âôØG'GŽöĨŪū¸€ęg…gŽŧ3ØŅ{ōā/Į<ŋí;ĩK˙rã+ĘŒūō×?zįøŖrɨZEų*—ß˙đė/zĪ>ž-bną3ņAĐŤ:šy—?ŧôĻ=ē†R† €:œ’õeį-´Åi•c=ÅĢÕĖ`:CŠšÜĸ&›“Éô$e“ŨiÆi[ŅēŽéÅUÁD4Ã6ØMEĨŦœĨ€|v¸d“ão†ä&éÖ°[ãt’ážâašœJĻīá4mqZåHdö´W>“LĘȧ“à įōøE?Geŗ(#Ôá{mî%u1ІBąäī¯ô45[ mÎ:(ÃYĩhĘ@#§¨%Ôl°ĘņŦh™¸lm°ĐōũĸxÖÕÕÛNGüsw`"ß/E5—Ÿû:ˇz,Š0r 95EyģÃLj6­č(ŗŗNˆŋ¨ŲT*OŗfCvö¤–ËȨą1ōƒũƒÅ­–Iô 3öšķvrŸ”`ZZoÍYK'ä~É+ėôE3VOøƒxØ/ØŋÖÜKK+t c(††œU ?œÎ-iJ”ĩÉS“<đVn{ËŌŨvĨÔĄĖNģžėKĢädO†m°›ŠĶ¯x8ļäžP-ķ5‡ žŽÃž[Û?—ËĄ”™ÁYÔLĸ;CÛmĖōßZĻn¨éxtîU5ŨŸĖä5Zv š‚ÍÁŦŦRéjņ”Q~0Ú§Û3äcŅÄ īî›/1âãdÄ%Ŗœ¸Åaáp×ĪMîFûåe_(ų1ĶĘņĄâ“ųlV­™ŊŖeöld*ˆĀÕj§íøž¤ÕœÆÔ×ÛL€–Ë(%‹:ß/ÁāœåLļ6Žĩiųtr0anēũh>35ޜT´Ų‹ ë*Ę]]+˙ķØč訉k__ūųØ .Ŧ(_mß°fläŌ˙ģ`ÕÚgÖíŲa{ĨÄ:WÅæ›ũČšÔÄÕķĒíVį2×īĮMb_¯ŗō“˙ûņå3į (Ŗ^ܴᇠĪUßq đ`XŋYÜģúȇŸĨÆ.PF=o~nC% æM{Ŧ§Ždޟ˙ŨØXáiaëÚE+kÕu˙å\ø÷ČŠņ‰/ŽV<[]um|eO—¨zeĪ+Ļß?̜9w@ÕÚj{Ņ€ĒWöŧ<ÕwæũO•“įnŨ6I— Vlløķ]7>yôzîĸöâŗTūĸ†rjŅĪķUoŲŌ<ōīG2WOœžÚŧ…1áÂcÜ%Qf‡[t¸ĩė`rÉŨņ”ÍĶjņík4h–ŊSu3 Åá7<§S˜ú7;lÎŊŽx€įhŗķĐĪųš÷ĖM’?hæ$ ËųĨ&3°’•<åx ņ¸Ēj”ŲŲ~ČÍ ūím!ˇ‹fhš††%ĢÜÚ`(”RŒYīÎnķŪp§ģ5Üۄ¨ €ļ íš•“I8$)bp7'jÅØ÷JõuĘ Ô&kM-M—TM‹Į´ĪÍÅ›ŋķe…Y›KčBY]‹×û*G[<aķJī.ķĘÖō†­­­ŅE34CĪ™v´ŨūœÄ8‚ąāŧÕwŋŋ}¨ÍËG5PŦŗũo‚–‰ļŊUđtyŦ,ö‹CģÛDûģgŌŌ‘Ža{{p%á¨|`_GZÎ)…@ŖËė öY—TqМ&Îīę ¸]Ũ MĶ”>+š(šÁFî(ëZ$ÉÕnÅŨ‚dĀrâ! äÕĄˆ7ÔĻę:h‹ŗ=ÜZ S¨ƒRDqÎßĖEŲK%@ÕĩtļÚŋöjy>ám ĨuЖ†öC6  ė­-l[ŗ7Ō&3XvģË,ŠNwŠpE)u(Įûš7§ŒÃ'WĶ)΄[5‡ážČPŽ`5&C`07uvųŦ%sĐ2á}ė˜ĸ¤ŧBŸÍ8șPŠėkkSĄÃ`á|‡ũ6jŲoĨ높ŠvÄ]ŗ7¨Ēg{BĄ€ĸë0ÔÖˇ„Ûo1îRŠôlÄ+t(ĒÆØ<‡‚vĐr‘Žaƒ`]­Č@Œø8ą¸įķE‡s9û-žpØÅæ"šœAj~ĩ‚bœŊĸ ŲžŽXšx–*ų]2Uë!߁ŪQƒ:]ß.ĩ˜hŠ;ŊIÕH[{ÃQ÷Ŧb%$yXÎÛĸBĻ4ŋĪė÷ }4C3´Rb4×ŌĄã9iÛ9ĀĀ8uųŨ‡ƒš7ESĐfŽ5ȁ@(ō'_}õU*•˛Ûl9zīŊ÷ž÷ŊīŨ!Ųo~ķ›]ģv-zøûß˙ū…^XđhöjūÕ{ūûë;ĒIi˙q0˙őá‰JĒĸ|fâŌ—į•0/ũ¤õģ¤Ķ/ûà +*Ļ59Ģ|Ža­ũåŸ6¸õ§›ãyetlîįûėÖæĩįF¸7˛q1ŧøJjĘėj÷9hR8„‡¸DŅÉՋœéū˛É%‡ÍžÃf"ū(ŋEŒHŒH ūˆ(yģtéR:žråĘĖĖĸ_ZCYYYuuõæÍ›Ÿyæ™ģ¯@=¤]¤‰ˆ‰‰ĘL*bųvŗlämëÖ­ĶĶĨĪĄ•••FRv@ đvŪ ƒÁ@6ģ@  ĢH@ qہ@ @œ7@ @ ΁@ @ ūH˜Ŋ°äʕ+$앁đm‚´ 411"1"Qųđä“O’ĒøX8o_UT?:ü€ō!žU};iibbDbDĸō#`üËĪČīŧ}ģų˙ûlŲ&_ĖĶéIENDŽB`‚django-q2-1.7.4/docs/admin.rst000066400000000000000000000074671471170400300160720ustar00rootroot00000000000000.. _admin_page: .. py:currentmodule:: django_q Admin pages =========== Django Q2 does not use custom pages, but instead leverages what is offered by Django's model admin by default. When you open Django Q2's admin pages you will see three models: Successful tasks ---------------- Shows all successfully executed tasks. Meaning they did not encounter any errors during execution. From here you can look at details of each task or delete them. Use the group filter to filter your results by schedule name or group id. The table is searchable by `name`, `func` and `group` .. image:: _static/successful.png Uses the :class:`Success` proxy model. .. tip:: The maximum number of successful tasks can be set using the :ref:`save_limit` option. Failed tasks ------------ Failed tasks have encountered an error, preventing them from finishing execution. The worker will try to put the error in the `result` field of the task so you can review what happened. You can resubmit a failed task back to the queue using the admins action menu. Uses the :class:`Failure` proxy model Customize the admin UI by creating your own ``admin.ModelAdmin`` class and use ``admin.site.unregister`` and ``admin.site.register`` to replace the default for example: .. code-block:: python from django_q import models as q_models from django_q import admin as q_admin admin.site.unregister([q_models.Failure]) @admin.register(q_models.Failure) class ChildClassAdmin(q_admin.FailAdmin): list_display = ( 'name', 'func', 'result', 'started', # add attempt_count to list_display 'attempt_count' ) Scheduled tasks --------------- Here you can check on the status of your scheduled tasks, create, edit or delete them. .. image:: _static/scheduled.png Repeats ~~~~~~~ If you want a schedule to only run a finite amount of times, e.g. every hour for the next 24 hours, you can do that using the :attr:`Schedule.repeats` attribute. In this case you would set the schedule type to :attr:`Schedule.HOURLY` and the repeats to `24`. Every time the schedule runs the repeats count down until it hits zero and schedule is no longer run. When you set repeats to ``-1`` the schedule will continue indefinitely and the repeats will still count down. This can be used as an indicator of how many times the schedule has been executed. An exception to this are schedules of type :attr:`Schedule.ONCE`. Negative repeats for this schedule type will cause it to be deleted from the database. This behavior is useful if you have many delayed actions which you do not necessarily need a result for. A positive number will keep the ONCE schedule, but it will not run again. You can pause a schedule by setting its repeats value to zero. .. note:: To run a ``ONCE`` schedule again, change the repeats to something other than `0`. Set a new run time before you do this or let it execute immediately. Next run ~~~~~~~~ Shows you when this task will be added to the queue next. Last run ~~~~~~~~ Links to the task result of the last scheduled run. Shows nothing if the schedule hasn't run yet or if task result has been deleted. Success ~~~~~~~ Indicates the success status of the last scheduled task, if any. .. note:: if you have set the :ref:`save_limit` configuration option to not save successful tasks to the database, you will only see the failed results of your schedules. Uses the :class:`Schedule` model Queued tasks ------------ This admin view is only enabled when you use the :ref:`orm_broker` broker. It shows all tasks packages currently in the broker queue. The ``lock`` column shows the moment at which this package was picked up by the cluster and is used to determine whether it has expired or not. For development purposes you can edit and delete queued tasks from here. django-q2-1.7.4/docs/architecture.rst000066400000000000000000000102251471170400300174460ustar00rootroot00000000000000Architecture ------------ .. image:: _static/cluster.png :alt: Django Q2 schema Signed Tasks """""""""""" Tasks are first pickled and then signed using Django's own :mod:`django.core.signing` module using the ``SECRET_KEY`` and cluster name as salt, before being sent to a message broker. This ensures that task packages on the broker can only be executed and read by clusters and django servers who share the same secret key and cluster name. If a package fails to unpack, it will be marked failed with the broker and discarded. Optionally the packages can be compressed before transport. Broker """""" The broker collects task packages from the django instances and queues them for pick up by a cluster. If the broker supports message receipts, it will keep a copy of the tasks around until a cluster acknowledges the processing of the task. Otherwise it is put back in the queue after a timeout period. This ensure at-least-once delivery. Most failed deliveries will be the result of a worker or the cluster crashing before the task was saved. .. note:: When the :ref:`ack_failures` option is set to ``False`` (the default), a task is considered a failed delivery when it raises an ``Exception``. Set this option to ``True`` to acknowledge failed tasks as successful. Pusher """""" The pusher process continuously checks the broker for new task packages. It checks the signing and unpacks the task to the internal Task Queue. The amount of tasks in the Task Queue can be configured to control memory usage and minimize data loss in case of a failure. Worker """""" A worker process pulls a task of the Task Queue and it sets a shared countdown timer with :ref:`sentinel` indicating it is about to start work. The worker then tries to execute the task and afterwards the timer is reset and any results (including errors) are saved to the package. Irrespective of the failure or success of any of these steps, the package is then pushed onto the Result Queue. Monitor """"""" The result monitor checks the Result Queue for processed packages and saves both failed and successful packages to the Django database or cache backend. If the broker supports it, a delivery receipt is sent. In case the task was part of a chain, the next task is queued. .. _sentinel: Sentinel """""""" The sentinel spawns all process and then checks the health of all workers, including the pusher and the monitor. This includes checking timers on each worker for timeouts. In case of a sudden death or timeout, it will reincarnate the failing processes. When a stop signal is received, the sentinel will halt the pusher and instruct the workers and monitor to finish the remaining items. See :ref:`stop_procedure` Timeouts """""""" Before each task execution the worker sets a countdown timer on the sentinel and resets it again after execution. Meanwhile the sentinel checks if the timers don't reach zero, in which case it will terminate the worker and reincarnate a new one. Scheduler """"""""" Twice a minute the scheduler checks for any scheduled tasks that should be starting. - Creates a task from the schedule - Subtracts 1 from :attr:`django_q.Schedule.repeats` - Sets the next run time if there are repeats left or if it has a negative value. .. _stop_procedure: Stop procedure """""""""""""" When a stop signal is received, the sentinel exits the guard loop and instructs the pusher to stop pushing. Once this is confirmed, the sentinel pushes poison pills onto the task queue and will wait for all the workers to exit. This ensures that the task queue is emptied before the workers exit. Afterwards the sentinel waits for the monitor to empty the result queue and the stop procedure is complete. - Send stop event to pusher - Wait for pusher to exit - Put poison pills in the Task Queue - Wait for all the workers to clear the queue and stop - Put a poison pill on the Result Queue - Wait for monitor to process remaining results and exit - Signal that we have stopped .. warning:: If you force the cluster to terminate before the stop procedure has completed, you can lose tasks or results still being held in memory. You can manage the amount of tasks in a clusters memory by setting the :ref:`queue_limit`. django-q2-1.7.4/docs/brokers.rst000066400000000000000000000160031471170400300164330ustar00rootroot00000000000000Brokers ======= The broker sits between your Django instances and your Django Q2 cluster instances; accepting, saving and delivering task packages. Currently we support a variety of brokers. The default Redis broker does not support message receipts. This means that in case of a catastrophic failure of the cluster server or worker timeouts, tasks that were being executed get lost. Keep in mind this is not the same as a failing task. If a tasks code crashes, this should only lead to a failed task status. Even though this might be acceptable in some use cases, you might prefer brokers with message receipts support. These guarantee delivery by waiting for the cluster to send a receipt after the task has been processed. In case a receipt has not been received after a set time, the task package is put back in the queue. Django Q2 supports this behavior by setting the :ref:`retry` timer on brokers that support message receipts. Some pointers: * Don't set the :ref:`retry` timer to a lower or equal number than the task timeout. * Retry time includes time the task spends waiting in the clusters internal queue. * Don't set the :ref:`queue_limit` so high that tasks time out while waiting to be processed. * In case a task is worked on twice, the task result will be updated with the latest results. * In some rare cases a non-atomic broker will re-queue a task after it has been acknowledged. * If a task runs twice and a previous run has succeeded, the new result will be discarded. * Limiting the number of retries is handled globally in your actual broker's settings. Support for more brokers is being worked on. Redis ----- The default broker for Django Q2 clusters. * Atomic * Requires `Redis-py `__ client library: ``pip install redis`` * Does not need cache framework for monitoring * Does not support receipts * Can use existing :ref:`django_redis` connections. * Configure with :ref:`redis_configuration`-py compatible configuration IronMQ ------ This HTTP based queue service is both available directly via `Iron.io `__ and as an add-on on Heroku. * Delivery receipts * Supports bulk dequeue * Needs Django's `Cache framework `__ configured for monitoring * Requires the `iron-mq `__ client library: ``pip install iron-mq`` * See the :ref:`ironmq_configuration` configuration section for options. Amazon SQS ---------- Amazon's Simple Queue Service is another HTTP based message queue. Although `SQS `__ is not the fastest, it is stable, cheap and convenient if you already use AWS. * Delivery receipts * Maximum message size is 256Kb * Supports bulk dequeue up to 10 messages with a maximum total size of 256Kb * Needs Django's `Cache framework `__ configured for monitoring * Requires the `boto3 `__ client library: ``pip install boto3`` * See the :ref:`sqs_configuration` configuration section for options. MongoDB ------- This highly scalable NoSQL database makes for a very fast and reliably persistent at-least-once message broker. Usually available on most PaaS providers, as `open-source `__ or commercial `enterprise `__ edition. * Delivery receipts * Needs Django's `Cache framework `__ configured for monitoring * Can be configured as the Django cache-backend through several open-source cache providers. * Requires the `pymongo `__ driver: ``pip install pymongo`` * See the :ref:`mongo_configuration` configuration section for options. .. _orm_broker: Django ORM ---------- Select this to use Django's database backend as a message broker. Unless you have configured a dedicated database backend for it, this should probably not be your first choice for a high traffic setup. However for a medium message rate and scheduled tasks, this is the most convenient guaranteed delivery broker. * Delivery receipts * Supports bulk dequeue * Needs Django's `Cache framework `__ configured for monitoring * Can be `configured `__ as its own cache backend. * Queue editable in Django Admin * See the :ref:`orm_configuration` configuration on how to set it up. Custom Broker ------------- You can override the :class:`Broker` or any of its existing derived broker types. .. code-block:: python # example Custom broker.py from django_q.brokers import Broker class CustomBroker(Broker): def info(self): return 'My Custom Broker' Using the :ref:`broker_class` configuration setting you can then instruct Django Q2 to use this instead of one of the existing brokers: .. code-block:: python # example Custom broker class connection Q_CLUSTER = { 'name': 'Custom', 'workers': 8, 'timeout': 60, 'broker_class: 'myapp.broker.CustomBroker' } If you do write a custom broker for one of the many message queueing servers out there we don't support yet, please consider contributing it to the project. Reference --------- The :class:`Broker` class is used internally to communicate with the different types of brokers. You can override this class if you want to contribute and support your own broker. .. py:class:: Broker .. py:method:: async_task(task) Sends a task package to the broker queue and returns a tracking id if available. .. py:method:: dequeue() Gets packages from the broker and returns a list of tuples with a tracking id and the package. .. py:method:: acknowledge(id) Notifies the broker that the task has been processed. Only works with brokers that support delivery receipts. .. py:method:: fail(id) Tells the broker that the message failed to be processed by the cluster. Only available on brokers that support this. Currently only occurs when a cluster fails to unpack a task package. .. py:method:: delete(id) Instructs the broker to delete this message from the queue. .. py:method:: purge_queue() Empties the current queue of all messages. .. py:method:: delete_queue() Deletes the current queue from the broker. .. py:method:: queue_size() Returns the amount of messages in the brokers queue. .. py:method:: lock_size() Optional method that returns the number of messages currently awaiting acknowledgement. Only implemented on brokers that support it. .. py:method:: ping() Returns True if the broker can be reached. .. py:method:: info() Shows the name and version of the currently configured broker. .. py:function:: brokers.get_broker() Returns a :class:`Broker` instance based on the current configuration. django-q2-1.7.4/docs/chain.rst000066400000000000000000000063101471170400300160460ustar00rootroot00000000000000.. py:currentmodule:: django_q Chains ====== Sometimes you want to run tasks sequentially. For that you can use the :func:`async_chain` function: .. code-block:: python # async a chain of tasks from django_q.tasks import async_chain, result_group # the chain must be in the format # [(func,(args),{kwargs}),(func,(args),{kwargs}),..] group_id = async_chain([('math.copysign', (1, -1)), ('math.floor', (1,))]) # get group result result_group(group_id, count=2) A slightly more convenient way is to use a :class:`Chain` instance: .. code-block:: python # Chain async from django_q.tasks import Chain # create a chain that uses the cache backend chain = Chain(cached=True) # add some tasks chain.append('math.copysign', 1, -1) chain.append('math.floor', 1) # run it chain.run() print(chain.result()) .. code-block:: python [-1.0, 1] Reference --------- .. py:function:: async_chain(chain, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None) Async a chain of tasks. See also the :class:`Chain` class. :param list chain: a list of tasks in the format [(func,(args),{kwargs}), (func,(args),{kwargs})] :param str group: an optional group name. :param bool cached: run this against the cache backend :param bool sync: execute this inline instead of asynchronous :param broker: an optional broker instance .. py:class:: Chain(chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None) A sequential chain of tasks. Acts as a convenient wrapper for :func:`async_chain` You can pass the task chain at construction or you can append individual tasks before running them. :param list chain: a list of task in the format [(func,(args),{kwargs}), (func,(args),{kwargs})] :param str group: an optional group name. :param bool cached: run this against the cache backend :param bool sync: execute this inline instead of asynchronous :param bool sync: execute this inline instead of asynchronous :param broker: an optional broker instance .. py:method:: append(func, *args, **kwargs) Append a task to the chain. Takes the same arguments as :func:`async_task` :return: the current number of tasks in the chain :rtype: int .. py:method:: run() Start queueing the chain to the worker cluster. :return: the chains group id .. py:method:: result(wait=0) return the full list of results from the chain when it finishes. Blocks until timeout or result. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of results .. py:method:: fetch(failures=True, wait=0) get the task result objects from the chain when it finishes. Blocks until timeout or result. :param failures: include failed tasks :param int wait: how many milliseconds to wait for a result :return: an unsorted list of task objects .. py:method:: current() get the index of the currently executing chain element :return int: current chain index .. py:method:: length() get the length of the chain :return int: length of the chain django-q2-1.7.4/docs/cluster.rst000066400000000000000000000153411471170400300164510ustar00rootroot00000000000000 Cluster ======= .. py:currentmodule:: django_q Django Q2 uses Python's multiprocessing module to manage a pool of workers that will handle your tasks. Start your cluster using Django's ``manage.py`` command:: $ python manage.py qcluster You should see the cluster starting :: 10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten starting. 10:57:40 [Q] INFO Process-ede257774c4444c980ab479f10947acc ready for work at 31784 10:57:40 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 ready for work at 31785 10:57:40 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 ready for work at 31786 10:57:40 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 ready for work at 31787 10:57:40 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b ready for work at 31788 10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31789 10:57:40 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b ready for work at 31790 10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31791 10:57:40 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 monitoring at 31792 10:57:40 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d guarding cluster at 31783 10:57:40 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 pushing tasks at 31793 10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten running. Stopping the cluster with ctrl-c or either the ``SIGTERM`` and ``SIGKILL`` signals, will initiate the :ref:`stop_procedure`:: 16:44:12 [Q] INFO Q Cluster freddie-uncle-twenty-ten stopping. 16:44:12 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d stopping cluster processes 16:44:13 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 stopped pushing tasks 16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work 16:44:13 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b stopped doing work 16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work 16:44:13 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b stopped doing work 16:44:13 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 stopped doing work 16:44:13 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 stopped doing work 16:44:13 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 stopped doing work 16:44:13 [Q] INFO Process-ede257774c4444c980ab479f10947acc stopped doing work 16:44:14 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 stopped monitoring results 16:44:15 [Q] INFO Q Cluster freddie-uncle-twenty-ten has stopped. The number of workers, optional timeouts, recycles and cpu_affinity can be controlled via the :doc:`configure` settings. Multiple Clusters ----------------- You can have multiple clusters on multiple machines, working on the same queue as long as: - They connect to the same :doc:`broker`. - They use the same cluster name. See :doc:`configure` - They share the same ``SECRET_KEY`` for Django. .. _multiple-queues Multiple Queues ----------------- You can have multiple queues in one Django site, and use multiple cluster to work on each queue. Different queues are identified by different queue names which are also cluster names. To run an alternate cluster, e.g. to work on the 'long' queue, start your cluster with command:: # On Linux $ Q_CLUSTER_NAME=long python manage.py qcluster # On Windows $ python manage.py qcluster --name long You can set different Q_CLUSTER options for alternative clusters, such as 'timeout', 'queue_limit' and any other options which are valid in :doc:`configure`. See :ref:`alt-clusters`. .. note:: To use multiple queue, use the keyword argument `cluster` in async_task() and schedule(): * if `cluster` is not set (the default), async_task() and schedule() will be handled by the default cluster; * if `cluster` is set, only clusters with matching cluster name will run the task or do the schedule. Using a Procfile ---------------- If you host on `Heroku `__ or you are using `Honcho `__ you can start the cluster from a :file:`Procfile` with an entry like this:: worker: python manage.py qcluster Process managers ---------------- While you certainly can run a Django Q2 with a process manager like `Supervisor `__ or `Circus `__ it is not strictly necessary. The cluster has an internal sentinel that checks the health of all the processes and recycles or reincarnates according to your settings or in case of unexpected crashes. Because of the multiprocessing daemonic nature of the cluster, it is impossible for a process manager to determine the clusters health and resource usage. An example :file:`circus.ini` :: [circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:django_q] cmd = python manage.py qcluster numprocesses = 1 copy_env = True Note that we only start one process. It is not a good idea to run multiple instances of the cluster in the same environment since this does nothing to increase performance and in all likelihood will diminish it. Control your cluster using the ``workers``, ``recycle`` and ``timeout`` settings in your :doc:`configure` An example :file:`supervisor.conf` :: [program:django-q] command = python manage.py qcluster stopasgroup = true Supervisor's ``stopasgroup`` will ensure that the single process doesn't leave orphan process on stop or restart. Reference --------- .. py:class:: Cluster .. py:method:: start Spawns a cluster and then returns .. py:method:: stop Initiates :ref:`stop_procedure` and waits for it to finish. .. py:method:: stat returns a :class:`Stat` object with the current cluster status. .. py:attribute:: pid The cluster process id. .. py:attribute:: host The current hostname .. py:attribute:: sentinel returns the :class:`multiprocessing.Process` containing the :ref:`sentinel`. .. py:attribute:: timeout The clusters timeout setting in seconds .. py:attribute:: start_event A :class:`multiprocessing.Event` indicating if the :ref:`sentinel` has finished starting the cluster .. py:attribute:: stop_event A :class:`multiprocessing.Event` used to instruct the :ref:`sentinel` to initiate the :ref:`stop_procedure` .. py:attribute:: is_starting Bool. Indicating that the cluster is busy starting up .. py:attribute:: is_running Bool. Tells you if the cluster is up and running. .. py:attribute:: is_stopping Bool. Shows that the stop procedure has been started. .. py:attribute:: has_stopped Bool. Tells you if the cluster has finished the stop procedure django-q2-1.7.4/docs/conf.py000066400000000000000000000237531471170400300155430ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Django Q documentation build configuration file, created by # sphinx-quickstart on Fri Jun 26 22:18:36 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys myPath = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, myPath + "/../") os.environ["DJANGO_SETTINGS_MODULE"] = "django_q.tests.settings" nitpick_ignore = [("py:class", "datetime")] # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx_rtd_theme", "sphinx.ext.todo", "sphinx.ext.intersphinx", # 'sphinx.ext.autodoc' ] intersphinx_mapping = { "python": ("https://docs.python.org/3.8", None), "django": ( "https://docs.djangoproject.com/en/2.2/", "https://docs.djangoproject.com/en/2.2/_objects/", ), } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Django Q2" copyright = "2015-2021, Ilan Steemers - 2022, Stan Triepels" author = "Ilan Steemers, Stan Triepels" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "1.7" # The full version, including alpha/beta/rc tags. release = "1.7.4" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation.None html_theme_options = { # 'description': "A multiprocessing task queue for Django", # 'github_user': 'GDay', # 'github_repo': 'django-q2', # 'github_banner': True, } html_sidebars = { "**": [ "about.html", "navigation.html", "relations.html", "searchbox.html", ] } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "_static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "DjangoQ2doc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', # Latex figure (float) alignment # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "DjangoQ2.tex", "Django Q2 Documentation", "Ilan Steemers", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = '_static/logo_large.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "djangoq2", "Django Q2 Documentation", [author], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "DjangoQ2", "Django Q2 Documentation", author, "DjangoQ2", "A multiprocessing distributed task queue for Django.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False django-q2-1.7.4/docs/configure.rst000066400000000000000000000402541471170400300167520ustar00rootroot00000000000000Configuration ------------- .. py:currentmodule:: django_q Configuration is handled via the ``Q_CLUSTER`` dictionary in your :file:`settings.py` .. code:: python # settings.py example Q_CLUSTER = { 'name': 'myproject', 'workers': 8, 'recycle': 500, 'timeout': 60, 'compress': True, 'save_limit': 250, 'queue_limit': 500, 'cpu_affinity': 1, 'label': 'Django Q2', 'redis': { 'host': '127.0.0.1', 'port': 6379, 'db': 0, }, 'ALT_CLUSTERS': { 'long': { 'timeout': 3000, 'retry': 3600, 'max_attempts': 2, }, 'short': { 'timeout': 10, 'max_attempts': 1, }, } } All configuration settings are optional: .. _name: name ~~~~ Used to differentiate between projects using the same broker. On most broker types this will be used as the queue name. Defaults to ``'default'``. .. note:: Tasks are signed. When a worker encounters a task with an invalid signature, it will be discarded or failed. workers ~~~~~~~ The number of workers to use in the cluster. Defaults to CPU count of the current host, but can be set to a custom number. [#f1]_ daemonize_workers ~~~~~~~~~~~~~~~~~ Set the daemon flag when spawning workers. You may need to disable this flag if your worker needs to spawn child process but be careful with orphaned child processes in case of sudden termination of the main process. Defaults to ``True``. recycle ~~~~~~~ The number of tasks a worker will process before recycling . Useful to release memory resources on a regular basis. Defaults to ``500``. max_rss ~~~~~~~ The maximum resident set size in kilobytes before a worker will recycle and release resources. Useful for limiting memory usage. Only supported on platforms that implement the python resource module or install the :ref:`psutil` module. Defaults to ``None``. .. _timeout: timeout ~~~~~~~ The number of seconds a worker is allowed to spend on a task before it's terminated. Defaults to ``None``, meaning it will never time out. Set this to something that makes sense for your project. Can be overridden for individual tasks. Note: for systems that don't have `SIGALRM` available (e.g. Windows), it will not raise an error properly. It will kill the task, but it will keep retrying until it finishes within the given time. See :ref:`retry` for details how to set values for timeout and retry. .. _time_zone: time_zone ~~~~~~~ The timezone that is used for task scheduling. Use this if you are having issue with DST. The scheduler uses UTC to calculate the next date and will therefore ignore any DST changes. This will cause 1 hour or 0.5 hour changes in the schedule when time is moved one hour ahead or back. Defaults to `settings.TIME_ZONE` if `USE_TZ` is enabled. .. _ack_failures: ack_failures ~~~~~~~~~~~~ When set to ``True``, also acknowledge unsuccessful tasks. This causes failed tasks to be considered as successful deliveries, thereby removing them from the task queue. Can also be set per-task by passing the ``ack_failure`` option to :func:`async_task`. Defaults to ``False``. .. _max_attempts: max_attempts ~~~~~~~~~~~~~ Limit the number of retry attempts for failed tasks. Set to 0 for infinite retries. Defaults to 0 .. _retry: retry ~~~~~ The number of seconds a broker will wait for a cluster to finish a task, before it's presented again. Only works with brokers that support delivery receipts. Defaults to 60. The value must be bigger than the time it takes to complete longest task, i.e. :ref:`timeout` must be less than retry value and all tasks must complete in less time than the selected retry time. If this does not hold, i.e. the retry value is less than timeout or less than it takes to finish a task, Django-Q2 will start the task again if the used broker supports receipts. For example, with the following code .. code:: python # settings.py Q_CLUSTER = { 'retry': 5, 'workers': 4, 'orm': 'default', } # example.py from django_q.tasks import async_task async_task('time.sleep', 22) First, ``time.sleep`` is called by the first worker. After 5 seconds second worker will also call ``time.sleep`` because retry time has exceeded and the broker return the task again for the cluster. After 21 seconds from the call to ``async_task`` all four workers are running the ``time.sleep(22)`` call and there is one retry in queue; tasks are started after 0, 5, 10, 15 and 20 seconds after the ``async_task`` was called. After 22 seconds the first worker completes and the task is acknowledged in the broker and the task is not added to task queue anymore but the task that was already in the run queue will run also. So in this example, ``time.sleep`` was called 5 times. Note also that the above issue might cause all workers to run the same long running task preventing new tasks from starting shortly after the task has been started by ``async_task``. In this case the retry time handling could cause the task that has not been started by any worker to be put on work queue again (even multiple times). compress ~~~~~~~~ Compresses task packages to the broker. Useful for large payloads, but can add overhead when used with many small packages. Defaults to ``False`` .. _save_limit: save_limit ~~~~~~~~~~ Limits the amount of successful tasks saved to Django. - Set to ``0`` for unlimited. - Set to ``-1`` for no success storage at all. - Defaults to ``250`` - Failures are always saved. save_limit_per ~~~~~~~~~~~~~~ The above ``save_limit`` for successful tasks can be fine tuned per task type using - Set to ``"group"`` to store the tasks per group - Other possible values are ``"func"``, ``"name"``, ``None`` - Defaults to ``None`` guard_cycle ~~~~~~~~~~~ Guard loop sleep in seconds, must be greater than 0 and less than 60. .. _sync: sync ~~~~ When set to ``True`` this configuration option forces all :func:`async_task` calls to be run with ``sync=True``. Effectively making everything synchronous. Useful for testing. Defaults to ``False``. .. _queue_limit: queue_limit ~~~~~~~~~~~ This does not limit the amount of tasks that can be queued on the broker, but rather how many tasks are kept in memory by a single cluster. Setting this to a reasonable number, can help balance the workload and the memory overhead of each individual cluster. Defaults to ``workers**2``. label ~~~~~ The label used for the Django Admin page. Defaults to ``'Django Q2'`` .. _catch_up: catch_up ~~~~~~~~ The default behavior for schedules that didn't run while a cluster was down, is to play catch up and execute all the missed time slots until things are back on schedule. You can override this behavior by setting ``catch_up`` to ``False``. This will make those schedules run only once when the cluster starts and normal scheduling resumes. Defaults to ``True``. .. _redis_configuration: redis ~~~~~ Connection settings for Redis. Defaults:: # redis defaults Q_CLUSTER = { 'redis': { 'host': 'localhost', 'port': 6379, 'db': 0, 'password': None, 'socket_timeout': None, 'charset': 'utf-8', 'errors': 'strict', 'unix_socket_path': None } } It's also possible to use a Redis connection URI:: Q_CLUSTER = { 'redis': 'redis://h:asdfqwer1234asdf@ec2-111-1-1-1.compute-1.amazonaws.com:111' } For more information on these settings please refer to the `Redis-py `__ documentation .. _django_redis: django_redis ~~~~~~~~~~~~ If you are already using `django-redis `__ for your caching, you can take advantage of its excellent connection backend by supplying the name of the cache connection you want to use instead of a direct Redis connection:: # example django-redis connection Q_CLUSTER = { 'name': 'DJRedis', 'workers': 4, 'timeout': 90, 'django_redis': 'default' } .. tip:: Django Q2 uses your ``SECRET_KEY`` to sign task packages and prevent task crossover. So make sure you have it set up in your Django settings. .. _ironmq_configuration: iron_mq ~~~~~~~ Connection settings for IronMQ:: # example IronMQ connection Q_CLUSTER = { 'name': 'IronBroker', 'workers': 8, 'timeout': 30, 'retry': 60, 'queue_limit': 50, 'bulk': 10, 'iron_mq': { 'host': 'mq-aws-us-east-1.iron.io', 'token': 'Et1En7.....0LuW39Q', 'project_id': '500f7b....b0f302e9' } } All connection keywords are supported. See the `iron-mq `__ library for more info .. _sqs_configuration: sqs ~~~ To use Amazon SQS as a broker you need to provide the AWS region and credentials either via the config, or any other boto3 configuration method:: # example SQS broker connection Q_CLUSTER = { 'name': 'SQSExample', 'workers': 4, 'timeout': 60, 'retry': 90, 'queue_limit': 100, 'bulk': 5, 'sqs': { 'aws_region': 'us-east-1', # optional 'aws_access_key_id': 'ac-Idr.....YwflZBaaxI', # optional 'aws_secret_access_key': '500f7b....b0f302e9' # optional } } Please make sure these credentials have proper SQS access. Amazon SQS only supports a bulk setting between 1 and 10, with the total payload not exceeding 256kb. .. _orm_configuration: orm ~~~ If you want to use Django's database backend as a message broker, set the ``orm`` keyword to the database connection you want it to use:: # example ORM broker connection Q_CLUSTER = { 'name': 'DjangORM', 'workers': 4, 'timeout': 90, 'retry': 120, 'queue_limit': 50, 'bulk': 10, 'orm': 'default' } Using the Django ORM backend will also enable the Queued Tasks table in the Admin. If you need better performance , you should consider using a different database backend than the main project. Set ``orm`` to the name of that database connection and make sure you run migrations on it using the ``--database`` option. .. _mongo_configuration: mongo ~~~~~ To use MongoDB as a message broker you simply provide the connection information in a dictionary:: # example MongoDB broker connection Q_CLUSTER = { 'name': 'MongoDB', 'workers': 8, 'timeout': 60, 'retry': 70, 'queue_limit': 100, 'mongo': { 'host': '127.0.0.1', 'port': 27017 } } The ``mongo`` dictionary can contain any of the parameters exposed by pymongo's `MongoClient `__ If you want to use a mongodb uri, you can supply it as the ``host`` parameter. mongo_db ~~~~~~~~ When using the MongoDB broker you can optionally provide a database name to use for the queues. Defaults to default database if available, otherwise ``django-q`` .. _broker_class: broker_class ~~~~~~~~~~~~ You can use a custom broker class for your cluster workers:: # example Custom broker class connection Q_CLUSTER = { 'name': 'Custom', 'workers': 8, 'timeout': 60, 'broker_class': 'myapp.broker.CustomBroker' } Make sure your ``CustomBroker`` class inherits from either the base :class:`Broker` class or one of its children. .. _bulk: bulk ~~~~ Sets the number of messages each cluster tries to get from the broker per call. Setting this on supported brokers can improve performance. Especially HTTP based or very high latency servers can benefit from bulk dequeue. Keep in mind however that settings this too high can degrade performance with multiple clusters or very large task packages. Not supported by the default Redis broker. Defaults to ``1``. poll ~~~~ Sets the queue polling interval for database brokers that don't have a blocking call. Currently only affects the ORM and MongoDB brokers. Defaults to ``0.2`` (seconds). cache ~~~~~ For some brokers, you will need to set up the Django `cache framework `__ to gather statistics for the monitor. You can indicate which cache to use by setting this value. Defaults to ``default``. .. _cached: cached ~~~~~~ Switches all task and result functions from using the database backend to the cache backend. This is the same as setting the keyword ``cached=True`` on all task functions. Instead of a bool this can also be set to the number of seconds you want the cache to retain results. e.g. ``cached=60`` scheduler ~~~~~~~~~ You can disable the scheduler by setting this option to ``False``. This will reduce a little overhead if you're not using schedules, but is most useful if you want to temporarily disable all schedules. Defaults to ``True`` .. _error_reporter: error_reporter ~~~~~~~~~~~~~~ You can redirect worker exceptions directly to various error reporters (for example `Rollbar `__ or `Sentry `__) by installing Django Q2 with the necessary `extras `__. To enable installed error reporters, you must provide the configuration settings required by an error reporter extension:: # error_reporter config--rollbar example Q_CLUSTER = { 'error_reporter': { 'rollbar': { 'access_token': '32we33a92a5224jiww8982', 'environment': 'Django-Q2' } } } For more information on error reporters and developing error reporting plugins for Django Q2, see :doc:`errors`. cpu_affinity ~~~~~~~~~~~~ Sets the number of processor each worker can use. This does not affect auxiliary processes like the sentinel or monitor and is only useful for tweaking the performance of very high traffic clusters. The affinity number has to be higher than zero and less than the total number of processors to have any effect. Defaults to using all processors:: # processor affinity example. 4 processors, 4 workers, cpu_affinity: 1 worker 1 cpu [0] worker 2 cpu [1] worker 3 cpu [2] worker 4 cpu [3] 4 processors, 4 workers, cpu_affinity: 2 worker 1 cpu [0, 1] worker 2 cpu [2, 3] worker 3 cpu [0, 1] worker 4 cpu [2, 3] 8 processors, 8 workers, cpu_affinity: 3 worker 1 cpu [0, 1, 2] worker 2 cpu [3, 4, 5] worker 3 cpu [6, 7, 0] worker 4 cpu [1, 2, 3] worker 5 cpu [4, 5, 6] worker 6 cpu [7, 0, 1] worker 7 cpu [2, 3, 4] worker 8 cpu [5, 6, 7] In some cases, setting the cpu affinity for your workers can lead to performance improvements, especially if the load is high and consists of many repeating small tasks. Start with an affinity of 1 and work your way up. You will have to experiment with what works best for you. As a rule of thumb; cpu_affinity 1 favors repetitive short running tasks, while no affinity benefits longer running tasks. .. note:: The ``cpu_affinity`` setting requires the optional :ref:`psutil` module. *Psutil does not support cpu affinity on OS X at this time.* .. _alt-clusters: ALT_CLUSTERS ~~~~~~~~~~~~ For multiple clusters working on multiple queues to run in one Django site. ALT_CLUSTERS should be a dict with cluster_name as its key, and the value is the configuration for the cluster with the key as its name. The configuration items are consistent with Q_CLUSTER, except for a few items such as name/cluster_name/ALT_CLUSTER, which are not available of course. See :ref:`multiple-queues`. .. note:: For a cluster, if its name is in ALT_CLUSTERS, the config item in ALT_CLUSTER will override the same config item in the Q_CLUSTER root. Other config items in Q_CLUSTER root remain in effect for this cluster. .. py:module:: django_q .. rubric:: Footnotes .. [#f1] Uses :func:`multiprocessing.cpu_count()` which can fail on some platforms. If so , please set the worker count in the configuration manually or install :ref:`psutil` to provide an alternative cpu count method. django-q2-1.7.4/docs/docker-compose.yaml000066400000000000000000000001401471170400300200230ustar00rootroot00000000000000services: docs: container_name: djangoq2-docs build: . volumes: - .:/docs django-q2-1.7.4/docs/errors.rst000066400000000000000000000023241471170400300163010ustar00rootroot00000000000000Errors ------ .. py:currentmodule:: django_q Django Q2 uses a pluggable error reporter system based upon python `extras `__, allowing anyone to develop plugins for their favorite error reporting and monitoring integration. Currently implemented examples include `Rollbar `__ and `Sentry `__. Error reporting plugins register a class which implements a ``report`` method, which is invoked when a Django Q2 cluster encounters an error, passing information to the particular service. Error reporters must be :ref:`configured` via the ``Q_CLUSTER`` dictionary in your :file:`settings.py`. These settings are passed as kwargs upon initiation of the Error Reporter. Therefore, in order to implement a new plugin, a package must expose a class which will be instantiated with the necessary information via the ``Q_CLUSTER`` settings and implements a single ``report`` method. For example implementations, see `django-q-rollbar `__ and `django-q-sentry `__ django-q2-1.7.4/docs/examples.rst000066400000000000000000000317201471170400300166050ustar00rootroot00000000000000Examples -------- .. py:currentmodule:: django_q Emails ====== Sending an email can take a while so why not queue it: .. code-block:: python # Welcome mail with follow up example from datetime import timedelta from django.utils import timezone from django_q.tasks import async_task, schedule from django_q.models import Schedule def welcome_mail(user): msg = 'Welcome to our website' # send this message right away async_task('django.core.mail.send_mail', 'Welcome', msg, 'from@example.com', [user.email]) # and this follow up email in one hour msg = 'Here are some tips to get you started...' schedule('django.core.mail.send_mail', 'Follow up', msg, 'from@example.com', [user.email], schedule_type=Schedule.ONCE, next_run=timezone.now() + timedelta(hours=1)) # since the `repeats` defaults to -1 # this schedule will erase itself after having run Since you're only telling Django Q2 to take care of the emails, you can quickly move on to serving web pages to your user. Signals ======= A good place to use async tasks are Django's model signals. You don't want to delay the saving or creation of objects, but sometimes you want to trigger a lot of actions: .. code-block:: python # Message on object change from django.contrib.auth.models import User from django.db.models.signals import pre_save from django.dispatch import receiver from django_q.tasks import async_task # set up the pre_save signal for our user @receiver(pre_save, sender=User) def email_changed(sender, instance, **kwargs): try: user = sender.objects.get(pk=instance.pk) except sender.DoesNotExist: pass # new user else: # has his email changed? if not user.email == instance.email: # tell everyone async_task('tasks.inform_everyone', instance) The task will send a message to everyone else informing them that the users email address has changed. Note that this adds almost no overhead to the save action: .. code-block:: python # tasks.py def inform_everyone(user): mails = [] for u in User.objects.exclude(pk=user.pk): msg = f"Dear {u.username}, {user.username} has a new email address: {user.email}" mails.append(('New email', msg, 'from@example.com', [u.email])) return send_mass_mail(mails) .. code-block:: python # or do it async again def inform_everyone_async(user): for u in User.objects.exclude(pk=user.pk): msg = f"Dear {u.username}, {user.username} has a new email address: {user.email}" async_task('django.core.mail.send_mail', 'New email', msg, 'from@example.com', [u.email]) Of course you can do other things beside sending emails. These are just generic examples. You can use signals with async to update fields in other objects too. Let's say this users email address is not just on the User object, but you stored it in some other places too without a reference. By attaching an async action to the save signal, you can now update that email address in those other places without impacting the the time it takes to return your views. Reports ======= In this example the user requests a report and we let the cluster do the generating, while handling the result with a hook. .. code-block:: python # Report generation with hook example from django_q.tasks import async_task # views.py # user requests a report. def create_report(request): async_task('tasks.create_html_report', request.user, hook='tasks.email_report') .. code-block:: python # tasks.py from django_q.tasks import async_task # report generator def create_html_report(user): html_report = 'We had a great quarter!' return html_report # report mailer def email_report(task): if task.success: # Email the report async_task('django.core.mail.send_mail', 'The report you requested', task.result, 'from@example.com', task.args[0].email) else: # Tell the admins something went wrong async_task('django.core.mail.mail_admins', 'Report generation failed', task.result) The hook is practical here, because it allows us to detach the sending task from the report generation function and to report on possible failures. Haystack ======== If you use `Haystack `__ as your projects search engine, here's an example of how you can have Django Q2 take care of your indexes in real time using model signals: .. code-block:: python # Real time Haystack indexing from .models import Document from django.db.models.signals import post_save from django.dispatch import receiver from django_q.tasks import async_task # hook up the post save handler @receiver(post_save, sender=Document) def document_changed(sender, instance, **kwargs): async_task('tasks.index_object', sender, instance, save=False) # turn off result saving to not flood your database .. code-block:: python # tasks.py from haystack import connection_router, connections def index_object(sender, instance): # get possible backends backends = connection_router.for_write(instance=instance) for backend in backends: # get the index for this model index = connections[backend].get_unified_index()\ .get_index(sender) # update it index.update_object(instance, using=backend) Now every time a Document is saved, your indexes will be updated without causing a delay in your save action. You could expand this to dealing with deletes, by adding a ``post_delete`` signal and calling ``index.remove_object`` in the async_task function. .. _shell: Shell ===== You can execute or schedule shell commands using Pythons :mod:`subprocess` module: .. code-block:: python from django_q.tasks import async_task, result # make a backup copy of setup.py async_task('subprocess.call', ['cp', 'setup.py', 'setup.py.bak']) # call ls -l and dump the output task_id=async_task('subprocess.check_output', ['ls', '-l']) # get the result dir_list = result(task_id) In Python 3.5 the subprocess module has changed quite a bit and returns a :class:`subprocess.CompletedProcess` object instead: .. code-block:: python from django_q.tasks import async_task, result # make a backup copy of setup.py tid = async_task('subprocess.run', ['cp', 'setup.py', 'setup.py.bak']) # get the result r=result(tid, 500) # we can now look at the original arguments >>> r.args ['cp', 'setup.py', 'setup.py.bak'] # and the returncode >>> r.returncode 0 # to capture the output we'll need a pipe from subprocess import PIPE # call ls -l and pipe the output tid = async_task('subprocess.run', ['ls', '-l'], stdout=PIPE) # get the result res = result(tid, 500) # print the output print(res.stdout) Instead of :func:`async_task` you can of course also use :func:`schedule` to schedule commands. For regular Django management commands, it is easier to call them directly: .. code-block:: python from django_q.tasks import async_task, schedule async_task('django.core.management.call_command','clearsessions') # or clear those sessions every hour schedule('django.core.management.call_command', 'clearsessions', schedule_type='H') Groups ====== A group example with Kernel density estimation for probability density functions using the Parzen-window technique. Adapted from `Sebastian Raschka's blog `__ .. code-block:: python # Group example with Parzen-window estimation import numpy from django_q.tasks import async_task, result_group, delete_group # the estimation function def parzen_estimation(x_samples, point_x, h): k_n = 0 for row in x_samples: x_i = (point_x - row[:, numpy.newaxis]) / h for row in x_i: if numpy.abs(row) > (1 / 2): break else: k_n += 1 return h, (k_n / len(x_samples)) / (h ** point_x.shape[1]) # create 100 calculations and return the collated result def parzen_async(): # clear the previous results delete_group('parzen', cached=True) mu_vec = numpy.array([0, 0]) cov_mat = numpy.array([[1, 0], [0, 1]]) sample = numpy.random. \ multivariate_normal(mu_vec, cov_mat, 10000) widths = numpy.linspace(1.0, 1.2, 100) x = numpy.array([[0], [0]]) # async_task them with a group label to the cache backend for w in widths: async_task(parzen_estimation, sample, x, w, group='parzen', cached=True) # return after 100 results return result_group('parzen', count=100, cached=True) Django Q2 is not optimized for distributed computing, but this example will give you an idea of what you can do with task :doc:`group`. Alternatively the ``parzen_async()`` function can also be written with :func:`async_iter`, which automatically utilizes the cache backend and groups to return a single result from an iterable: .. code-block:: python # create 100 calculations and return the collated result def parzen_async(): mu_vec = numpy.array([0, 0]) cov_mat = numpy.array([[1, 0], [0, 1]]) sample = numpy.random. \ multivariate_normal(mu_vec, cov_mat, 10000) widths = numpy.linspace(1.0, 1.2, 100) x = numpy.array([[0], [0]]) # async_task them with async_task iterable args = [(sample, x, w) for w in widths] result_id = async_iter(parzen_estimation, args, cached=True) # return the cached result or timeout after 10 seconds return result(result_id, wait=10000, cached=True) Http Health Check ================= An example of a python http server you can use (localhost:8080) to validate the health status of all the clusters in your environment. Example is http only. Requires cache to be enabled. Save file in your Django project's root directory and run with command: ``python worker_hc.py`` in your container or other environment. Can be customized to show whatever you'd like from the Stat class or modified as needed. .. code-block:: python from http.server import BaseHTTPRequestHandler, HTTPServer from mtt_app.settings.base import EMAIL_USE_TLS import os import django # Set the correct path to you settings module os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my.settings.path") # All django stuff has to come after the setup: django.setup() from django_q.status import Stat from django_q.conf import Conf # Set host and port settings hostName = "localhost" serverPort = 8080 class HealthCheckServer(BaseHTTPRequestHandler): def do_GET(self): # Count the clusters and their status happy_clusters = 0 total_clusters = 0 for stat in Stat.get_all(): total_clusters += 1 if stat.status in [Conf.IDLE, Conf.WORKING]: happy_clusters += 1 # Return 200 response if there is at least 1 cluster running, # and make sure all running clusters are happy if total_clusters and happy_clusters == total_clusters: response_code = 200 else: response_code = 500 self.send_response(response_code) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write( bytes("Django-Q Heath Check", "utf-8") ) self.wfile.write( bytes(f"

Health check returned {response_code} response

", "utf-8") ) self.wfile.write( bytes( f"

{happy_clusters} of {total_clusters} cluster(s) are happy

", "utf-8", ) ) if __name__ == "__main__": webServer = HTTPServer((hostName, serverPort), HealthCheckServer) print("Server started at http://%s:%s" % (hostName, serverPort)) try: webServer.serve_forever() except KeyboardInterrupt: pass webServer.server_close() print("Server stopped.") .. note:: If you have an example you want to share, please submit a pull request on `github `__. django-q2-1.7.4/docs/group.rst000066400000000000000000000077621471170400300161340ustar00rootroot00000000000000.. py:currentmodule:: django_q Groups ====== You can group together results by passing :func:`async_task` the optional ``group`` keyword: .. code-block:: python # result group example from django_q.tasks import async_task, result_group for i in range(4): async_task('math.modf', i, group='modf') # wait until the group has 4 results result = result_group('modf', count=4) print(result) .. code-block:: python [(0.0, 0.0), (0.0, 1.0), (0.0, 2.0), (0.0, 3.0)] Note that this particular example can be achieved much faster with :doc:`iterable` Take care to not limit your results database too much and call :func:`delete_group` before each run, unless you want your results to keep adding up. Instead of :func:`result_group` you can also use :func:`fetch_group` to return a queryset of :class:`Task` objects.: .. code-block:: python # fetch group example from django_q.tasks import fetch_group, count_group, result_group # count the number of failures failure_count = count_group('modf', failures=True) # only use the successes results = fetch_group('modf') if failure_count: results = results.exclude(success=False) results = [task.result for task in successes] # this is the same as results = fetch_group('modf', failures=False) results = [task.result for task in successes] # and the same as results = result_group('modf') # filters failures by default Getting results by using :func:`result_group` is of course much faster than using :func:`fetch_group`, but it doesn't offer the benefits of Django's queryset functions. You can also access group functions from a task result instance: .. code-block:: python from django_q.tasks import fetch task = fetch('winter-speaker-alpha-ceiling') if task.group_count() > 100: print(task.group_result()) task.group_delete() print('Deleted group {}'.format(task.group)) or call them directly on :class:`AsyncTask` object: .. code-block:: python from django_q.tasks import AsyncTask # add a task to the math group and run it cached a = AsyncTask('math.floor', 2.5, group='math', cached=True) # wait until this tasks group has 10 results result = a.result_group(count=10) Reference --------- .. py:function:: result_group(group_id, failures=False, wait=0, count=None, cached=False) Returns the results of a task group :param str group_id: the group identifier :param bool failures: set this to ``True`` to include failed results :param int wait: optional milliseconds to wait for a result or count. -1 for indefinite :param int count: block until there are this many results in the group :param bool cached: run this against the cache backend :returns: a list of results :rtype: list .. py:function:: fetch_group(group_id, failures=True, wait=0, count=None, cached=False) Returns a list of tasks in a group :param str group_id: the group identifier :param bool failures: set this to ``False`` to exclude failed tasks :param int wait: optional milliseconds to wait for a task or count. -1 for indefinite :param int count: block until there are this many tasks in the group :param bool cached: run this against the cache backend. :returns: a list of :class:`Task` :rtype: list .. py:function:: count_group(group_id, failures=False, cached=False) Counts the number of task results in a group. :param str group_id: the group identifier :param bool failures: counts the number of failures if ``True`` :param bool cached: run this against the cache backend. :returns: the number of tasks or failures in a group :rtype: int .. py:function:: delete_group(group_id, tasks=False, cached=False) Deletes a group label from the database. :param str group_id: the group identifier :param bool tasks: also deletes the associated tasks if ``True`` :param bool cached: run this against the cache backend. :returns: the numbers of tasks affected :rtype: int django-q2-1.7.4/docs/index.rst000066400000000000000000000031621471170400300160750ustar00rootroot00000000000000.. Django Q documentation master file, created by sphinx-quickstart on Fri Jun 26 22:18:36 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Django Q2 ==================== Django Q2 is a native Django task queue, scheduler and worker application using Python multiprocessing. .. note:: Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development of Django Q has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q Features -------- - Multiprocessing worker pools - Asynchronous tasks - Scheduled, cron and repeated tasks - Signed and compressed packages - Failure and success database or cache - Result hooks, groups and chains - Django Admin integration - PaaS compatible with multiple instances - Multi cluster monitor - Redis, IronMQ, SQS, MongoDB or ORM - Rollbar and Sentry support Django Q2 is tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.x and 5.0.x Currently available in English, German and French. Contents: .. toctree:: :maxdepth: 2 Installation Configuration Brokers Tasks Groups Iterable Chains Schedules Cluster Monitor Admin Errors Signals Architecture Examples * :ref:`genindex` django-q2-1.7.4/docs/install.rst000066400000000000000000000153401471170400300164350ustar00rootroot00000000000000Installation ============ .. py:currentmodule:: django_q - Install the latest version with pip:: $ pip install django-q2 - Add :mod:`django_q` to ``INSTALLED_APPS`` in your projects :file:`settings.py`:: INSTALLED_APPS = ( # other apps 'django_q', ) - Run Django migrations to create the database tables:: $ python manage.py migrate - Choose a message :doc:`broker`, configure it and install the appropriate client library. - Run Django Q2 cluster in order to handle tasks async:: $ python manage.py qcluster Migrate from Django-Q to Django-Q2 ---------------------------------- If you have an application with django-q running right now, you can simply swap the libraries and you should be good to go.:: $ pip uninstall django-q # you might have to uninstall django-q add-ons as well $ pip install django-q2 Then migrate the database to get the latest tables/fields:: $ python manage.py migrate Requirements ------------ Django Q2 is tested for Python 3.8, 3.9, 3.10, 3.11 and 3.12 - `Django `__ Django Q2 aims to use as much of Django's standard offerings as possible. The code is tested against Django versions `3.2.x`, `4.1.x`, `4.2.x` and `5.0.x`. - `Django-picklefield `__ Used to store args, kwargs and result objects in the database. Optional ~~~~~~~~ - `Blessed `__ is used to display the statistics in the terminal:: $ pip install blessed - `Redis-py `__ client by Andy McCurdy is used to interface with both the Redis:: $ pip install redis .. _psutil_package: - `Psutil `__ python system and process utilities module by Giampaolo Rodola', is an optional requirement and adds cpu affinity settings to the cluster:: $ pip install psutil - `setproctitle `__ python module to customize the process title by Daniele Varrazzo', is an optional requirement used to set informative process titles:: $ pip install setproctitle - `Hiredis `__ parser. This C library maintained by the core Redis team is faster than the standard PythonParser during high loads:: $ pip install hiredis - `Boto3 `__ is used for the Amazon SQS broker in favor of the now deprecating boto library:: $ pip install boto3 - `Iron-mq `_ is the official python binding for the IronMQ broker:: $ pip install iron-mq - `Pymongo `__ is needed if you want to use MongoDB as a message broker:: $ pip install pymongo - `Redis `__ server is the default broker for Django Q2. It provides the best performance and does not require Django's cache framework for monitoring. - `MongoDB `__ is a highly scalable NoSQL database which makes for a very fast and reliably persistent at-least-once message broker. Usually available on most PaaS providers. - `Pyrollbar `__ is an error notifier for `Rollbar `__ which lets you manage your worker errors in one place. Needs a `Rollbar `__ account and access key:: $ pip install rollbar .. _croniter_package: - `Croniter `__ is an optional package that is used to parse cron expressions for the scheduler:: $ pip install croniter Add-ons ------- - `django-q-rollbar `__ is a Rollbar error reporter:: $ pip install django-q2[rollbar] - `django-q-sentry `__ is a Sentry error reporter:: $ pip install django-q2[sentry] - `django-q-email `__ is a compatible Django email backend that will automatically async queue your emails. OS X ~~~~ Running Django Q2 on OS X should work fine, except for the following known issues: * :meth:`multiprocessing.Queue.qsize()` is not supported. This leads to the monitor not reporting the internal queue size of clusters running under OS X. * CPU count through :func:`multiprocessing.cpu_count()` does not work. Installing :ref:`psutil` provides Django Q2 with an alternative way of determining the number of CPU's on your system * CPU affinity is provided by :ref:`psutil` which at this time does not support this feature on OSX. The code however is aware of this and will fake the CPU affinity assignment in the logs without actually assigning it. This way you can still develop with this setting. Windows ~~~~~~~ The cluster and worker multiprocessing code depend on the OS's ability to fork, unfortunately forking is not supported under windows. You should however be able to develop and test without the cluster by setting the ``sync`` option to ``True`` in the configuration. This will run all ``async`` calls inline through a single cluster worker without the need for forking. Other known issues are: * :func:`os.getppid()` is only supported under windows since Python 3.2. If you use an older version you need to install :ref:`psutil` as an alternative. * CPU count through :func:`multiprocessing.cpu_count()` occasionally fails on servers. Installing :ref:`psutil` provides Django Q2 with an alternative way of determining the number of CPU's on your system * The monitor and info commands rely on the Curses package which is not officially supported on windows. There are however some ports available like `this one `__ by Christoph Gohlke. Python ~~~~~~ Current tests are performed with 3.8, 3.9, 3.10, 3.11 and 3.12 If you do encounter any regressions with earlier versions, please submit an issue on `github `__ Open-source packages ~~~~~~~~~~~~~~~~~~~~ Django Q2 is always tested with the latest versions of the required and optional Python packages. We try to keep the dependencies as up to date as possible. You can reference the `requirements `__ file to determine which versions are currently being used for tests and development. Django ~~~~~~ We strive to be compatible with the last two major version of Django. At the moment this means we support the 3.2.x, 4.1.x, 4.2.x and 5.0.x releases. Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.8 For this you can always use older releases, but they are no longer maintained. django-q2-1.7.4/docs/iterable.rst000066400000000000000000000061121471170400300165530ustar00rootroot00000000000000.. py:currentmodule:: django_q Iterable ======== If you have an iterable object with arguments for a function, you can use :func:`async_iter` to async them with a single command:: # Async Iterable example from django_q.tasks import async_iter, result # set up a list of arguments for math.floor iter = [i for i in range(100)] # async_task iter them id=async_iter('math.floor',iter) # wait for the collated result for 1 second result_list = result(id, wait=1000) This will individually queue 100 tasks to the worker cluster, which will save their results in the cache backend for speed. Once all the 100 results are in the cache, they are collated into a list and saved as a single result in the database. The cache results are then cleared. You can also use an :class:`Iter` instance which can sometimes be more convenient: .. code-block:: python from django_q.tasks import Iter i = Iter('math.copysign') # add some arguments i.append(1, -1) i.append(2, -1) i.append(3, -1) # run it i.run() # get the results print(i.result()) .. code-block:: python [-1.0, -2.0, -3.0] Reference --------- .. py:function:: async_iter(func, args_iter,**kwargs) Runs iterable arguments against the cache backend and returns a single collated result. Accepts the same options as :func:`async_task` except ``hook``. See also the :class:`Iter` class. :param object func: The task function to execute :param args: An iterable containing arguments for the task function :param dict kwargs: Keyword arguments for the task function. Ignores ``hook``. :returns: The uuid of the task :rtype: str .. py:class:: Iter(func=None, args=None, kwargs=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None) An async task with iterable arguments. Serves as a convenient wrapper for :func:`async_iter` You can pass the iterable arguments at construction or you can append individual argument tuples. :param func: the function to execute :param args: an iterable of arguments. :param kwargs: the keyword arguments :param bool cached: run this against the cache backend :param bool sync: execute this inline instead of asynchronous :param broker: optional broker instance .. py:method:: append(*args) Append arguments to the iter set. Returns the current set count. :param args: the arguments for a single execution :return: the current set count :rtype: int .. py:method:: run() Start queueing the tasks to the worker cluster. :return: the task result id .. py:method:: result(wait=0) return the full list of results. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of results .. py:method:: fetch(wait=0) get the task result objects. :param int wait: how many milliseconds to wait for a result :return: an unsorted list of task objects .. py:method:: length() get the length of the arguments list :return int: length of the argument list django-q2-1.7.4/docs/monitor.rst000066400000000000000000000117771471170400300164700ustar00rootroot00000000000000Monitor ======= .. py:currentmodule::django_q.monitor .. warning:: Blessed needs to be installed to get this to work! See: https://pypi.org/project/blessed/ The cluster monitor shows live information about all the Q clusters connected to your project. Start the monitor with Django's `manage.py` command:: $ python manage.py qmonitor .. image:: _static/monitor.png For all broker types except the Redis broker, the monitor utilizes Django's cache framework to store statistics of running clusters. This can be any type of cache backend as long as it can be shared among Django instances. For this reason, the local memory backend will not work. Legend ------ Host ~~~~ Shows the hostname of the server this cluster is running on. Id ~~ The cluster Id. Same as the cluster process ID or pid. State ~~~~~ Current state of the cluster: - **Starting** The cluster is spawning workers and getting ready. - **Idle** Everything is ok, but there are no tasks to process. - **Working** Processing tasks like a good cluster should. - **Stopping** The cluster does not take on any new tasks and is finishing. - **Stopped** All tasks have been processed and the cluster is shutting down. Pool ~~~~ The current number of workers in the cluster pool. TQ ~~ **Task Queue** counts the number of tasks in the queue [#f1]_ If this keeps rising it means you are taking on more tasks than your cluster can handle. You can limit this by settings the :ref:`queue_limit` in your cluster configuration, after which it will turn green when that limit has been reached. If your task queue is always hitting its limit and your running out of resources, it may be time to add another cluster. RQ ~~ **Result Queue** shows the number of results in the queue. [#f1]_ Since results are only saved by a single process which has to access the database. It's normal for the result queue to take slightly longer to clear than the task queue. RC ~~ **Reincarnations** shows the amount of processes that have been reincarnated after a recycle, sudden death or timeout. If this number is unusually high, you are either suffering from repeated task errors or severe timeouts and you should check your logs for details. Up ~~ **Uptime** the amount of time that has passed since the cluster was started. .. centered:: Press `q` to quit the monitor and return to your terminal. Info ---- If you just want to see a one-off summary of your cluster stats you can use the `qinfo` management command:: $ python manage.py qinfo .. image:: _static/info.png All stats are summed over all available clusters. Task rate is calculated over the last 24 hours and will show the number of tasks per second, minute, hour or day depending on the amount. Average execution time (`Avg time`) is calculated in seconds over the last 24 hours. Since some of these numbers are based on what is available in your tasks database, limiting or disabling the result backend will skew them. Like with the monitor, these statistics come from a Redis server or Django's cache framework. So make sure you have either one configured. To print out the current configuration run:: $ python manage.py qinfo --config Status ------ You can check the status of your clusters straight from your code with the :class:`Stat` class: .. code:: python from django_q.status import Stat for stat in Stat.get_all(): print(stat.cluster_id, stat.status) # or if you know the cluster id cluster_id = 1234 stat = Stat.get(cluster_id) print(stat.status, stat.workers) Reference --------- .. py:class:: Stat Cluster status object. .. py:attribute:: cluster_id Id of this cluster. Corresponds with the process id. .. py:attribute:: tob Time Of Birth .. py:method:: uptime Shows the number of seconds passed since the time of birth .. py:attribute:: reincarnations The number of times the sentinel had to start a new worker process. .. py:attribute:: status String representing the current cluster status. .. py:attribute:: task_q_size The number of tasks currently in the task queue. [#f1]_ .. py:attribute:: done_q_size The number of tasks currently in the result queue. [#f1]_ .. py:attribute:: pusher The pid of the pusher process .. py:attribute:: monitor The pid of the monitor process .. py:attribute:: sentinel The pid of the sentinel process .. py:attribute:: workers A list of process ids of the workers currently in the cluster pool. .. py:method:: empty_queues Returns true or false depending on any tasks still present in the task or result queue. .. py:classmethod:: get(cluster_id, broker=None) Gets the current :class:`Stat` for the cluster id. Takes an optional broker connection. .. py:classmethod:: get_all(broker=None) Returns a list of :class:`Stat` objects for all active clusters. Takes an optional broker connection. .. rubric:: Footnotes .. [#f1] Uses :meth:`multiprocessing.Queue.qsize()` which is not implemented on OS X and always returns 0. django-q2-1.7.4/docs/requirements.txt000066400000000000000000000000301471170400300175070ustar00rootroot00000000000000sphinx-rtd-theme==1.0.0 django-q2-1.7.4/docs/schedules.rst000066400000000000000000000223261471170400300167500ustar00rootroot00000000000000Schedules ========= .. py:currentmodule:: django_q Schedule -------- Schedules are regular Django models. You can manage them through the :ref:`admin_page` or directly from your code with the :func:`schedule` function or the :class:`Schedule` model: .. code:: python # Use the schedule wrapper from django_q.tasks import schedule schedule('math.copysign', 2, -2, hook='hooks.print_result', schedule_type='D') # Or create the object directly from django_q.models import Schedule Schedule.objects.create(func='math.copysign', hook='hooks.print_result', args='2,-2', schedule_type=Schedule.DAILY ) # In case you want to use q_options # Specify the broker by using the property broker_name in q_options schedule('math.sqrt', 9, hook='hooks.print_result', q_options={'timeout': 30, 'broker_name': 'broker_1'}, schedule_type=Schedule.HOURLY) # Run a schedule every 5 minutes, starting at 6 today # for 2 hours from datetime import datetime schedule('math.hypot', 3, 4, schedule_type=Schedule.MINUTES, minutes=5, repeats=24, next_run=datetime.utcnow().replace(hour=18, minute=0)) # Use a cron expression schedule('math.hypot', 3, 4, schedule_type=Schedule.CRON, cron = '0 22 * * 1-5') # Restrain a schedule to a specific cluster schedule('math.hypot', 3, 4, schedule_type=Schedule.DAILY, cluster='my_cluster') Missed schedules ---------------- If your cluster has not run for a while, the default behavior for the scheduler is to play catch up with the schedules and keep executing them until they are up to date. In practical terms this means the scheduler will execute tasks in the past, reschedule them in the past and immediately execute them again until the schedule is set in the future. This default behavior is intended to facilitate schedules that poll or gather statistics, but might not be suitable to your particular situation. You can change this by setting the :ref:`catch_up` configuration setting to ``False``. The scheduler will then skip execution of scheduled events in the past. Instead those tasks will run once when the cluster starts again and the scheduler will find the next available slot in the future according to original schedule parameters. When :ref:`catch_up` is to ``True`` it may be useful for the task to know what was the date and time it was originally intended to run at. To achieve this, pass an identifier name to parameter `intended_date_kwarg` when creating the schedule. The intended datetime will then be passed - in isoformat string - as a kwarg with that identifier name to the task that has been created. Management Commands ------------------- If you want to schedule regular Django management commands, you can use the :mod:`django.core.management` module to call them directly: .. code-block:: python from django_q.tasks import schedule # run `manage.py clearsession` every hour schedule('django.core.management.call_command', 'clearsessions', schedule_type='H') Or you can make a wrapper function which you can then schedule in Django Q: .. code-block:: python # tasks.py from django.core import management # wrapping `manage.py clearsessions` def clear_sessions_command(): return management.call_command('clearsessions') # now you can schedule it to run every hour from django_q.tasks import schedule schedule('tasks.clear_sessions_command', schedule_type='H') Check out the :ref:`shell` examples if you want to schedule regular shell commands .. note:: Schedules needs the optional :ref:`Croniter` package installed to parse cron expressions. Reference --------- .. py:function:: schedule(func, *args, name=None, hook=None, schedule_type='O', minutes=None, repeats=-1, next_run=now() , q_options=None, **kwargs) Creates a schedule :param str func: the function to schedule. Dotted strings only. :param args: arguments for the scheduled function. :param str name: An optional name for your schedule. :param str hook: optional result hook function. Dotted strings only. :param str schedule_type: (O)nce, M(I)nutes, (H)ourly, (D)aily, (W)eekly, (M)onthly, (Q)uarterly, (Y)early or (C)ron :attr:`Schedule.TYPE` :param int minutes: Number of minutes for the Minutes type. :param str cron: Cron expression for the Cron type. :param int repeats: Number of times to repeat schedule. -1=Always, 0=Never, n =n. :param datetime next_run: Next or first scheduled execution datetime. :param str cluster: optional cluster name. Task will be executed only on a cluster with a matching :ref:`name`. :param str intended_date_kwarg: optional identifier to pass intended schedule date. :param dict q_options: options passed to async_task for this schedule :param kwargs: optional keyword arguments for the scheduled function. .. note:: q_options does not accept the 'broker' key with a broker instance but accepts a 'broker_name' key instead. This can be used to specify the broker connection name to assign the task. If a broker with the specified name does not exist or is not running at the moment of placing the task in queue it fallbacks to the random broker/queue that handled the schedule. .. class:: Schedule A database model for task schedules. .. py:attribute:: id Primary key .. py:attribute:: name A name for your schedule. Tasks created by this schedule will assume this or the primary key as their group id. .. py:attribute:: func The function to be scheduled .. py:attribute:: hook Optional hook function to be called after execution. .. py:attribute:: args Positional arguments for the function. .. py:attribute:: kwargs Keyword arguments for the function .. py:attribute:: schedule_type The type of schedule. Follows :attr:`Schedule.TYPE` .. py:attribute:: TYPE :attr:`ONCE`, :attr:`MINUTES`, :attr:`HOURLY`, :attr:`DAILY`, :attr:`WEEKLY`, :attr:`BIWEEKLY`, :attr:`MONTHLY`, :attr:`BIMONTHLY`, :attr:`QUARTERLY`, :attr:`YEARLY`, :attr:`CRON` .. py:attribute:: minutes The number of minutes the :attr:`MINUTES` schedule should use. Is ignored for other schedule types. .. py:attribute:: cron A cron string describing the schedule. You need the optional `croniter` package installed for this. .. py:attribute:: repeats Number of times to repeat the schedule. -1=Always, 0=Never, n =n. When set to -1, this will keep counting down. .. py:attribute:: cluster Task will be executed only on a cluster with a matching :ref:`name`. .. py:attribute:: intended_date_kwarg Name of kwarg to pass intended schedule date. .. py:attribute:: next_run Datetime of the next scheduled execution. .. py:attribute:: task Id of the last task generated by this schedule. .. py:method:: last_run() Admin link to the last executed task. .. py:method:: success() Returns the success status of the last executed task. .. py:attribute:: ONCE `'O'` the schedule will only run once. If it has a negative :attr:`repeats` it will be deleted after it has run. If you want to keep the result, set :attr:`repeats` to a positive number. .. py:attribute:: MINUTES `'I'` will run every :attr:`minutes` after its first run. .. py:attribute:: HOURLY `'H'` the scheduled task will run every hour after its first run. .. py:attribute:: DAILY `'D'` the scheduled task will run every day at the time of its first run. .. py:attribute:: WEEKLY `'W'` the task will run every week on they day and time of the first run. .. py:attribute:: BIWEEKLY `'BW'` the task will run once every two weeks on they day and time of the first run. .. py:attribute:: MONTHLY `'M'` the tasks runs every month on they day and time of the last run. .. note:: Months are tricky. If you schedule something on the 31st of the month and the next month has only 30 days or less, the task will run on the last day of the next month. It will however continue to run on that day, e.g. the 28th, in subsequent months. .. py:attribute:: BIMONTHLY `'BM'` the tasks runs once every two months on they day and time of the last run. .. note:: Months are tricky. If you schedule something on the 31st of the month and the next month has only 30 days or less, the task will run on the last day of the next month. It will however continue to run on that day, e.g. the 28th, in subsequent months. .. py:attribute:: QUARTERLY `'Q'` this task runs once every 3 months on the day and time of the last run. .. py:attribute:: YEARLY `'Y'` only runs once a year. The same caution as with months apply; If you set this to february 29th, it will run on february 28th in the following years. .. py:attribute:: CRON `'C'` uses the optional `croniter` package to determine a schedule based on a cron expression. django-q2-1.7.4/docs/signals.rst000066400000000000000000000037421471170400300164320ustar00rootroot00000000000000Signals ======= .. py:currentmodule:: django_q Available signals ----------------- Django Q2 emits the following signals during its lifecycle. Before enqueuing a task """"""""""""""""""""""" The ``django_q.signals.pre_enqueue`` signal is emitted before a task is enqueued. The task dictionary is given as the ``task`` argument. After spawning a worker process """"""""""""""""""""""""""""""" The ``django_q.signals.post_spawn`` signal is emitted after a worker process has spawned. The process name is given as the ``proc_name`` argument (string). Before executing a task """"""""""""""""""""""" The ``django_q.signals.pre_execute`` signal is emitted before a task is executed by a worker. This signal provides two arguments: - ``task``: the task dictionary. - ``func``: the actual function that will be executed. If the task was created with a function path, this argument will be the callable function nonetheless. After executing a task """""""""""""""""""""" The ``django_q.signals.post_execute`` signal is emitted after a task is executed by a worker and processed by the monitor. It included the ``task`` dictionary with the result. Subscribing to a signal ----------------------- Connecting to a Django Q2 signal is done the same as any other Django signal:: from django.dispatch import receiver from django_q.signals import pre_enqueue, pre_execute, post_execute, post_spawn @receiver(pre_enqueue) def my_pre_enqueue_callback(sender, task, **kwargs): print(f"Task {task['name']} will be queued") @receiver(pre_execute) def my_pre_execute_callback(sender, func, task, **kwargs): print(f"Task {task['name']} will be executed by calling {func}") @receiver(post_execute) def my_post_execute_callback(sender, task, **kwargs): print(f"Task {task['name']} was executed with result {task['result']}") @receiver(post_spawn) def my_post_spawn_callback(sender, proc_name, **kwargs): print(f"Process {proc_name} has spawned") django-q2-1.7.4/docs/tasks.rst000066400000000000000000000365221471170400300161210ustar00rootroot00000000000000Tasks ===== .. py:currentmodule:: django_q .. _async: async_task() ------------ Use :func:`async_task` from your code to quickly offload tasks to the :class:`Cluster`: .. code:: python from django_q.tasks import async_task, result # create the task async_task('math.copysign', 2, -2) # or with import and storing the id import math.copysign task_id = async_task(copysign, 2, -2) # get the result task_result = result(task_id) # result returns None if the task has not been executed yet # you can wait for it task_result = result(task_id, 200) # but in most cases you will want to use a hook: async_task('math.modf', 2.5, hook='hooks.print_result') # hooks.py def print_result(task): print(task.result) :func:`async_task` can take the following optional keyword arguments: hook """" The function to call after the task has been executed. This function gets passed the complete :class:`Task` object as its argument. group """"" A group label. Check :doc:`group` for group functions. save """" Overrides the result backend's save setting for this task. timeout """"""" Overrides the cluster's timeout setting for this task. See :ref:`retry` for details how to set values for timeout. ack_failure """"""""""" Overrides the cluster's :ref:`ack_failures` setting for this task. sync """" Simulates a task execution synchronously. Useful for testing. Can also be forced globally via the :ref:`sync` configuration option. cached """""" Redirects the result to the cache backend instead of the database if set to ``True`` or to an integer indicating the cache timeout in seconds. e.g. ``cached=60``. Especially useful with large and group operations where you don't need the all results in your database and want to take advantage of the speed of your cache backend. broker """""" A broker instance, in case you want to control your own connections. cluster """""" The name of the cluster. Only useful if you are using [alternative queues](https://django-q2.readthedocs.io/en/master/cluster.html#multiple-queues). task_name """"""""" Optionally overwrites the auto-generated task name. q_options """"""""" None of the option keywords get passed on to the task function. As an alternative you can also put them in a single keyword dict named ``q_options``. This enables you to use these keywords for your function call:: # Async options in a dict opts = {'hook': 'hooks.print_result', 'group': 'math', 'timeout': 30} async_task('math.modf', 2.5, q_options=opts) Please note that this will override any other option keywords. .. note:: For tasks to be processed you will need to have a worker cluster running in the background using ``python manage.py qcluster`` or you need to configure Django Q2 to run in synchronous mode for testing using the :ref:`sync` option. AsyncTask --------- Optionally you can use the :class:`AsyncTask` class to instantiate a task and keep everything in a single object.: .. code-block:: python # AsyncTask class instance example from django_q.tasks import AsyncTask # instantiate an async task a = AsyncTask('math.floor', 1.5, group='math') # you can set or change keywords afterwards a.cached = True # run it a.run() # wait indefinitely for the result and print it # don't let the task return `None` or it will wait indefinitely print(a.result(wait=-1)) # change the args a.args = (2.5,) # run it again a.run() # wait max 10 seconds for the result and print it print(a.result(wait=10)) .. code-block:: python 1 2 Once you change any of the parameters of the task after it has run, the result is invalidated and you will have to :func:`AsyncTask.run` it again to retrieve a new result. Cached operations ----------------- You can run your tasks results against the Django cache backend instead of the database backend by either using the global :ref:`cached` setting or by supplying the ``cached`` keyword to individual functions. This can be useful if you are not interested in persistent results or if you run large group tasks where you only want the final result. By using a cache backend like Redis or Memcached you can speed up access to your task results significantly compared to a relational database. When you set ``cached=True``, results will be saved permanently in the cache and you will have to rely on your backend's cleanup strategies (like LRU) to manage stale results. You can also opt to set a manual timeout on the results, by setting e.g. ``cached=60``. Meaning the result will be evicted from the cache after 60 seconds. This works both globally or on individual async executions.:: # simple cached example from django_q.tasks import async_task, result # cache the result for 10 seconds id = async_task('math.floor', 100, cached=10) # wait max 50ms for the result to appear in the cache result(id, wait=50, cached=True) # or fetch the task object task = fetch(id, cached=True) # and then save it to the database task.save() As you can see you can easily turn a cached result into a permanent database result by calling ``save()`` on it. This also works for group actions:: # cached group example from django_q.tasks import async_task, result_group from django_q.brokers import get_broker # set up a broker instance for better performance broker = get_broker() # Async a hundred functions under a group label for i in range(100): async_task('math.frexp', i, group='frexp', cached=True, broker=broker) # wait max 50ms for one hundred results to return result_group('frexp', wait=50, count=100, cached=True) If you don't need hooks, that exact same result can be achieved by using the more convenient :func:`async_iter`. Synchronous testing ------------------- :func:`async_task` can be instructed to execute a task immediately by setting the optional keyword ``sync=True``. The task will then be injected straight into a worker and the result saved by a monitor instance:: from django_q.tasks import async_task, fetch # create a synchronous task task_id = async_task('my.buggy.code', sync=True) # the task will then be available immediately task = fetch(task_id) # and can be examined if not task.success: print('An error occurred: {}'.format(task.result)) .. code:: bash An error occurred: ImportError("No module named 'my'",) Note that :func:`async_task` will block until the task is executed and saved. This feature bypasses the broker and is intended for debugging and development. Instead of setting ``sync`` on each individual ``async_task`` you can also configure :ref:`sync` as a global override. Connection pooling ------------------ Django Q2 tries to pass broker instances around its parts as much as possible to save you from running out of connections. When you are making individual calls to :func:`async_task` a lot though, it can help to set up a broker to reuse for :func:`async_task`: .. code:: python # broker connection economy example from django_q.tasks import async_task from django_q.brokers import get_broker broker = get_broker() for i in range(50): async_task('math.modf', 2.5, broker=broker) .. tip:: If you are using `django-redis `__ and the redis broker, you can :ref:`configure ` Django Q2 to use its connection pool. Reference --------- .. py:function:: async_task(func, *args, hook=None, group=None, timeout=None,\ save=None, sync=False, cached=False, broker=None, cluster=None, q_options=None, **kwargs) Puts a task in the cluster queue :param object func: The task function to execute :param tuple args: The arguments for the task function :param object hook: Optional function to call after execution :param str group: An optional group identifier :param int timeout: Overrides global cluster :ref:`timeout`. :param bool save: Overrides global save setting for this task. :param bool ack_failure: Overrides the global :ref:`ack_failures` setting for this task. :param bool sync: If set to True, async_task will simulate a task execution :param cached: Output the result to the cache backend. Bool or timeout in seconds :param broker: Optional broker connection from :func:`brokers.get_broker` :param cluster: Optional cluster name if using alternative queues :param dict q_options: Options dict, overrides option keywords :param dict kwargs: Keyword arguments for the task function :returns: The uuid of the task :rtype: str .. py:function:: result(task_id, wait=0, cached=False) Gets the result of a previously executed task :param str task_id: the uuid or name of the task :param int wait: optional milliseconds to wait for a result. -1 for indefinite, but be sure the result will not be `None` otherwise it will wait indefinitely! :param bool cached: run this against the cache backend. :returns: The result of the executed task .. py:function:: fetch(task_id, wait=0, cached=False) Returns a previously executed task :param str task_id: the uuid or name of the task :param int wait: optional milliseconds to wait for a result. -1 for indefinite :param bool cached: run this against the cache backend. :returns: A task object :rtype: Task .. versionchanged:: 0.2.0 Renamed from get_task .. py:function:: queue_size() Returns the size of the broker queue. Note that this does not count tasks currently being processed. :returns: The amount of task packages in the broker :rtype: int .. py:function:: delete_cached(task_id, broker=None) Deletes a task from the cache backend :param str task_id: the uuid of the task :param broker: an optional broker instance .. py:class:: Task Database model describing an executed task .. py:attribute:: id An :func:`uuid.uuid4()` identifier .. py:attribute:: name The name of the task as a humanized version of the :attr:`id` .. note:: This is for convenience and can be used as a parameter for most functions that take a `task_id`. Keep in mind that it is not guaranteed to be unique if you store very large amounts of tasks in the database. .. py:attribute:: func The function or reference that was executed .. py:attribute:: hook The function to call after execution. .. py:attribute:: args Positional arguments for the function. .. py:attribute:: kwargs Keyword arguments for the function. .. py:attribute:: result The result object. Contains the error if any occur. .. py:attribute:: started The moment the task was created by an async command .. py:attribute:: stopped The moment a worker finished this task .. py:attribute:: success Was the task executed without problems? .. py:method:: time_taken Calculates the difference in seconds between started and stopped. .. note:: Time taken represents the time a task spends in the cluster, this includes any time it may have waited in the queue. .. py:method:: group_result(failures=False) Returns a list of results from this task's group. Set failures to ``True`` to include failed results. .. py:method:: group_count(failures=False) Returns a count of the number of task results in this task's group. Returns the number of failures when ``failures=True`` .. py:method:: group_delete(tasks=False) Resets the group label on all the tasks in this task's group. If ``tasks=True`` it will also delete the tasks in this group from the database, including itself. .. py:classmethod:: get_result(task_id) Gets a result directly by task uuid or name. .. py:classmethod:: get_result_group(group_id, failures=False) Returns a list of results from a task group. Set failures to ``True`` to include failed results. .. py:classmethod:: get_task(task_id) Fetches a single task object by uuid or name. .. py:classmethod:: get_task_group(group_id, failures=True) Gets a queryset of tasks with this group id. Set failures to ``False`` to exclude failed tasks. .. py:classmethod:: get_group_count(group_id, failures=False) Returns a count of the number of tasks results in a group. Returns the number of failures when ``failures=True`` .. py:classmethod:: delete_group(group_id, objects=False) Deletes a group label only, by default. If ``objects=True`` it will also delete the tasks in this group from the database. .. py:class:: Success A proxy model of :class:`Task` with the queryset filtered on :attr:`Task.success` is ``True``. .. py:class:: Failure A proxy model of :class:`Task` with the queryset filtered on :attr:`Task.success` is ``False``. .. py:class:: AsyncTask(func, *args, **kwargs) A class wrapper for the :func:`async_task` function. :param object func: The task function to execute :param tuple args: The arguments for the task function :param dict kwargs: Keyword arguments for the task function, including async_task options .. py:attribute:: id The task unique identifier. This will only be available after it has been :meth:`run` .. py:attribute:: started Bool indicating if the task has been run with the current parameters .. py:attribute:: func The task function to execute .. py:attribute:: args A tuple of arguments for the task function .. py:attribute:: kwargs Keyword arguments for the function. Can include any of the optional async_task keyword attributes directly or in a `q_options` dictionary. .. py:attribute:: broker Optional :class:`Broker` instance to use .. py:attribute:: sync Run this task inline instead of asynchronous. .. py:attribute:: save Overrides the global save setting. .. py:attribute:: hook Optional function to call after a result is available. Takes the result :class:`Task` as the first argument. .. py:attribute:: group Optional group identifier .. py:attribute:: cached Run the task against the cache result backend. .. py:method:: run() Send the task to a worker cluster for execution .. py:method:: result(wait=0) The task result. Always returns None if the task hasn't been run with the current parameters. :param int wait: the number of milliseconds to wait for a result. -1 for indefinite .. py:method:: fetch(wait=0) Returns the full :class:`Task` result instance. :param int wait: the number of milliseconds to wait for a result. -1 for indefinite .. py:method:: result_group(failures=False, wait=0, count=None) Returns a list of results from this task's group. :param bool failures: set this to ``True`` to include failed results :param int wait: optional milliseconds to wait for a result or count. -1 for indefinite :param int count: block until there are this many results in the group .. py:method:: fetch_group(failures=True, wait=0, count=None) Returns a list of task results from this task's group :param bool failures: set this to ``False`` to exclude failed tasks :param int wait: optional milliseconds to wait for a task or count. -1 for indefinite :param int count: block until there are this many tasks in the group django-q2-1.7.4/exampleproject/000077500000000000000000000000001471170400300163245ustar00rootroot00000000000000django-q2-1.7.4/exampleproject/__init__.py000066400000000000000000000000001471170400300204230ustar00rootroot00000000000000django-q2-1.7.4/exampleproject/asgi.py000066400000000000000000000006251471170400300176240ustar00rootroot00000000000000""" ASGI config for exampleproject project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings") application = get_asgi_application() django-q2-1.7.4/exampleproject/settings.py000066400000000000000000000065351471170400300205470ustar00rootroot00000000000000""" Django settings for exampleproject project. Generated by 'django-admin startproject' using Django 5.1.1. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-)oouh93bjg+c=b!l5-*w)7et1l+!3nmp223rr^1r4v#jn&ow3f" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_q", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "exampleproject.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "exampleproject.wsgi.application" # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" Q_CLUSTER = { "name": "DjangORM", "workers": 4, "timeout": 90, "retry": 120, "queue_limit": 50, "bulk": 10, "orm": "default", } django-q2-1.7.4/exampleproject/urls.py000066400000000000000000000016461471170400300176720ustar00rootroot00000000000000""" URL configuration for exampleproject project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/5.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path from exampleproject import views urlpatterns = [ path("admin/", admin.site.urls), path("new_task/", views.add_task, name="add_task"), path("result//", views.get_result, name="get_result"), ] django-q2-1.7.4/exampleproject/views.py000066400000000000000000000013611471170400300200340ustar00rootroot00000000000000import time from django.http import HttpResponse from django.urls import reverse from django_q import tasks # internal function to be called with django_q def new_task(run_for_minutes): print("Task started") time.sleep(run_for_minutes) print("Task done") return True def add_task(request): task_id = tasks.async_task(new_task, 5) result_url = reverse("get_result", args=[task_id]) return HttpResponse( f"Added async task with Go to results" ) def get_result(request, task_id): task = tasks.fetch(task_id) if not task: msg = "Task running... please refresh after some time" else: msg = f"Async task result: {task.result}" return HttpResponse(msg) django-q2-1.7.4/exampleproject/wsgi.py000066400000000000000000000006251471170400300176520ustar00rootroot00000000000000""" WSGI config for exampleproject project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings") application = get_wsgi_application() django-q2-1.7.4/poetry.lock000066400000000000000000003456141471170400300155130ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = ">=3.6" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] [[package]] name = "ansicon" version = "1.89.0" description = "Python wrapper for loading Jason Hood's ANSICON" optional = true python-versions = "*" files = [ {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] [[package]] name = "asgiref" version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.dependencies] typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = true python-versions = ">=3.7" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] name = "babel" version = "2.15.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "backports-zoneinfo" version = "0.2.1" description = "Backport of the standard library zoneinfo module" optional = false python-versions = ">=3.6" files = [ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, ] [package.extras] tzdata = ["tzdata"] [[package]] name = "blessed" version = "1.20.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." optional = true python-versions = ">=2.7" files = [ {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, ] [package.dependencies] jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} six = ">=1.9.0" wcwidth = ">=0.1.4" [[package]] name = "boto3" version = "1.34.103" description = "The AWS SDK for Python" optional = true python-versions = ">=3.8" files = [ {file = "boto3-1.34.103-py3-none-any.whl", hash = "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365"}, {file = "boto3-1.34.103.tar.gz", hash = "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9"}, ] [package.dependencies] botocore = ">=1.34.103,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" version = "1.34.103" description = "Low-level, data-driven core of boto 3." optional = true python-versions = ">=3.8" files = [ {file = "botocore-1.34.103-py3-none-any.whl", hash = "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431"}, {file = "botocore-1.34.103.tar.gz", hash = "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, ] [package.extras] crt = ["awscrt (==0.20.9)"] [[package]] name = "certifi" version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.5.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "croniter" version = "2.0.5" description = "croniter provides iteration for datetime object with cron like format" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" files = [ {file = "croniter-2.0.5-py2.py3-none-any.whl", hash = "sha256:fdbb44920944045cc323db54599b321325141d82d14fa7453bc0699826bbe9ed"}, {file = "croniter-2.0.5.tar.gz", hash = "sha256:f1f8ca0af64212fbe99b1bee125ee5a1b53a9c1b433968d8bca8817b79d237f3"}, ] [package.dependencies] python-dateutil = "*" pytz = ">2021.1" [[package]] name = "django" version = "4.2.13" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, ] [package.dependencies] asgiref = ">=3.6.0,<4" "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-picklefield" version = "3.2" description = "Pickled object field for Django" optional = false python-versions = ">=3" files = [ {file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"}, {file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"}, ] [package.dependencies] Django = ">=3.2" [package.extras] tests = ["tox"] [[package]] name = "django-q-rollbar" version = "0.1.3" description = "A Rollbar support plugin for Django Q" optional = true python-versions = "^3.6" files = [ {file = "django-q-rollbar-0.1.3.tar.gz", hash = "sha256:a4de6da294507e6cbde77a015ece429f3602ea1d8df473ef2d63e44d2041ee4b"}, ] [package.dependencies] rollbar = ">=0.14.0,<0.15.0" [[package]] name = "django-q-sentry" version = "0.1.6" description = "A Sentry support plugin for Django Q" optional = true python-versions = "*" files = [ {file = "django-q-sentry-0.1.6.linux-x86_64.tar.gz", hash = "sha256:bdbd9c7a48d543c2d74ae9a4c9004dfc0965cb11bc145c6cf4de3429be1b58ae"}, {file = "django_q_sentry-0.1.6-py3-none-any.whl", hash = "sha256:9b8b4d7fad253a7d9a47f2c2ab0d9dea83078b7ef45c8849dbb1e4176ef8d050"}, ] [package.dependencies] sentry-sdk = ">=1.5.5" [[package]] name = "django-redis" version = "5.4.0" description = "Full featured redis cache backend for Django." optional = true python-versions = ">=3.6" files = [ {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, ] [package.dependencies] Django = ">=3.2" redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" [package.extras] hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] [[package]] name = "dnspython" version = "2.6.1" description = "DNS toolkit" optional = true python-versions = ">=3.8" files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, ] [package.extras] dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] dnssec = ["cryptography (>=41)"] doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] doq = ["aioquic (>=0.9.25)"] idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] [[package]] name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] [[package]] name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "hiredis" version = "2.3.2" description = "Python wrapper for hiredis" optional = true python-versions = ">=3.7" files = [ {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:742093f33d374098aa21c1696ac6e4874b52658c870513a297a89265a4d08fe5"}, {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9e14fb70ca4f7efa924f508975199353bf653f452e4ef0a1e47549e208f943d7"}, {file = "hiredis-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d7302b4b17fcc1cc727ce84ded7f6be4655701e8d58744f73b09cb9ed2b13df"}, {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed63e8b75c193c5e5a8288d9d7b011da076cc314fafc3bfd59ec1d8a750d48c8"}, {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b4edee59dc089bc3948f4f6fba309f51aa2ccce63902364900aa0a553a85e97"}, {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6481c3b7673a86276220140456c2a6fbfe8d1fb5c613b4728293c8634134824"}, {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684840b014ce83541a087fcf2d48227196576f56ae3e944d4dfe14c0a3e0ccb7"}, {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c4c0bcf786f0eac9593367b6279e9b89534e008edbf116dcd0de956524702c8"}, {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66ab949424ac6504d823cba45c4c4854af5c59306a1531edb43b4dd22e17c102"}, {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:322c668ee1c12d6c5750a4b1057e6b4feee2a75b3d25d630922a463cfe5e7478"}, {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa73e3f163c6e8b2ec26f22285d717a5f77ab2120c97a2605d8f48b26950dac"}, {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7f39f28ffc65de577c3bc0c7615f149e35bc927802a0f56e612db9b530f316f9"}, {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55ce31bf4711da879b96d511208efb65a6165da4ba91cb3a96d86d5a8d9d23e6"}, {file = "hiredis-2.3.2-cp310-cp310-win32.whl", hash = "sha256:3dd63d0bbbe75797b743f35d37a4cca7ca7ba35423a0de742ae2985752f20c6d"}, {file = "hiredis-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea002656a8d974daaf6089863ab0a306962c8b715db6b10879f98b781a2a5bf5"}, {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:adfbf2e9c38b77d0db2fb32c3bdaea638fa76b4e75847283cd707521ad2475ef"}, {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:80b02d27864ebaf9b153d4b99015342382eeaed651f5591ce6f07e840307c56d"}, {file = "hiredis-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd40d2e2f82a483de0d0a6dfd8c3895a02e55e5c9949610ecbded18188fd0a56"}, {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfa904045d7cebfb0f01dad51352551cce1d873d7c3f80c7ded7d42f8cac8f89"}, {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28bd184b33e0dd6d65816c16521a4ba1ffbe9ff07d66873c42ea4049a62fed83"}, {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f70481213373d44614148f0f2e38e7905be3f021902ae5167289413196de4ba4"}, {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8797b528c1ff81eef06713623562b36db3dafa106b59f83a6468df788ff0d1"}, {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02fc71c8333586871602db4774d3a3e403b4ccf6446dc4603ec12df563127cee"}, {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0da56915bda1e0a49157191b54d3e27689b70960f0685fdd5c415dacdee2fbed"}, {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e2674a5a3168349435b08fa0b82998ed2536eb9acccf7087efe26e4cd088a525"}, {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:dc1c3fd49930494a67dcec37d0558d99d84eca8eb3f03b17198424538f2608d7"}, {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:14c7b43205e515f538a9defb4e411e0f0576caaeeda76bb9993ed505486f7562"}, {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bac7e02915b970c3723a7a7c5df4ba7a11a3426d2a3f181e041aa506a1ff028"}, {file = "hiredis-2.3.2-cp311-cp311-win32.whl", hash = "sha256:63a090761ddc3c1f7db5e67aa4e247b4b3bb9890080bdcdadd1b5200b8b89ac4"}, {file = "hiredis-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:70d226ab0306a5b8d408235cabe51d4bf3554c9e8a72d53ce0b3c5c84cf78881"}, {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5c614552c6bd1d0d907f448f75550f6b24fb56cbfce80c094908b7990cad9702"}, {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c431431abf55b64347ddc8df68b3ef840269cb0aa5bc2d26ad9506eb4b1b866"}, {file = "hiredis-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a45857e87e9d2b005e81ddac9d815a33efd26ec67032c366629f023fe64fb415"}, {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138d141ec5a6ec800b6d01ddc3e5561ce1c940215e0eb9960876bfde7186aae"}, {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:387f655444d912a963ab68abf64bf6e178a13c8e4aa945cb27388fd01a02e6f1"}, {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4852f4bf88f0e2d9bdf91279892f5740ed22ae368335a37a52b92a5c88691140"}, {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d711c107e83117129b7f8bd08e9820c43ceec6204fff072a001fd82f6d13db9f"}, {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92830c16885f29163e1c2da1f3c1edb226df1210ec7e8711aaabba3dd0d5470a"}, {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:16b01d9ceae265d4ab9547be0cd628ecaff14b3360357a9d30c029e5ae8b7e7f"}, {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5986fb5f380169270a0293bebebd95466a1c85010b4f1afc2727e4d17c452512"}, {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:49532d7939cc51f8e99efc326090c54acf5437ed88b9c904cc8015b3c4eda9c9"}, {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8f34801b251ca43ad70691fb08b606a2e55f06b9c9fb1fc18fd9402b19d70f7b"}, {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7298562a49d95570ab1c7fc4051e72824c6a80e907993a21a41ba204223e7334"}, {file = "hiredis-2.3.2-cp312-cp312-win32.whl", hash = "sha256:e1d86b75de787481b04d112067a4033e1ecfda2a060e50318a74e4e1c9b2948c"}, {file = "hiredis-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:6dbfe1887ffa5cf3030451a56a8f965a9da2fa82b7149357752b67a335a05fc6"}, {file = "hiredis-2.3.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:4fc242e9da4af48714199216eb535b61e8f8d66552c8819e33fc7806bd465a09"}, {file = "hiredis-2.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e81aa4e9a1fcf604c8c4b51aa5d258e195a6ba81efe1da82dea3204443eba01c"}, {file = "hiredis-2.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:419780f8583ddb544ffa86f9d44a7fcc183cd826101af4e5ffe535b6765f5f6b"}, {file = "hiredis-2.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6871306d8b98a15e53a5f289ec1106a3a1d43e7ab6f4d785f95fcef9a7bd9504"}, {file = "hiredis-2.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb0b35b63717ef1e41d62f4f8717166f7c6245064957907cfe177cc144357c"}, {file = "hiredis-2.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c490191fa1218851f8a80c5a21a05a6f680ac5aebc2e688b71cbfe592f8fec6"}, {file = "hiredis-2.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4baf4b579b108062e91bd2a991dc98b9dc3dc06e6288db2d98895eea8acbac22"}, {file = "hiredis-2.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e627d8ef5e100556e09fb44c9571a432b10e11596d3c4043500080ca9944a91a"}, {file = "hiredis-2.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:ba3dc0af0def8c21ce7d903c59ea1e8ec4cb073f25ece9edaec7f92a286cd219"}, {file = "hiredis-2.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:56e9b7d6051688ca94e68c0c8a54a243f8db841911b683cedf89a29d4de91509"}, {file = "hiredis-2.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:380e029bb4b1d34cf560fcc8950bf6b57c2ef0c9c8b7c7ac20b7c524a730fadd"}, {file = "hiredis-2.3.2-cp37-cp37m-win32.whl", hash = "sha256:948d9f2ca7841794dd9b204644963a4bcd69ced4e959b0d4ecf1b8ce994a6daa"}, {file = "hiredis-2.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cfa67afe2269b2d203cd1389c00c5bc35a287cd57860441fb0e53b371ea6a029"}, {file = "hiredis-2.3.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bcbe47da0aebc00a7cfe3ebdcff0373b86ce2b1856251c003e3d69c9db44b5a7"}, {file = "hiredis-2.3.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f2c9c0d910dd3f7df92f0638e7f65d8edd7f442203caf89c62fc79f11b0b73f8"}, {file = "hiredis-2.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:01b6c24c0840ac7afafbc4db236fd55f56a9a0919a215c25a238f051781f4772"}, {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1f567489f422d40c21e53212a73bef4638d9f21043848150f8544ef1f3a6ad1"}, {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28adecb308293e705e44087a1c2d557a816f032430d8a2a9bb7873902a1c6d48"}, {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27e9619847e9dc70b14b1ad2d0fb4889e7ca18996585c3463cff6c951fd6b10b"}, {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0026cfbf29f07649b0e34509091a2a6016ff8844b127de150efce1c3aff60b"}, {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9de7586522e5da6bee83c9cf0dcccac0857a43249cb4d721a2e312d98a684d1"}, {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e58494f282215fc461b06709e9a195a24c12ba09570f25bdf9efb036acc05101"}, {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3a32b4b76d46f1eb42b24a918d51d8ca52411a381748196241d59a895f7c5c"}, {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1979334ccab21a49c544cd1b8d784ffb2747f99a51cb0bd0976eebb517628382"}, {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0c0773266e1c38a06e7593bd08870ac1503f5f0ce0f5c63f2b4134b090b5d6a4"}, {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bd1cee053416183adcc8e6134704c46c60c3f66b8faaf9e65bf76191ca59a2f7"}, {file = "hiredis-2.3.2-cp38-cp38-win32.whl", hash = "sha256:5341ce3d01ef3c7418a72e370bf028c7aeb16895e79e115fe4c954fff990489e"}, {file = "hiredis-2.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8fc7197ff33047ce43a67851ccf190acb5b05c52fd4a001bb55766358f04da68"}, {file = "hiredis-2.3.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:f47775e27388b58ce52f4f972f80e45b13c65113e9e6b6bf60148f893871dc9b"}, {file = "hiredis-2.3.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:9412a06b8a8e09abd6313d96864b6d7713c6003a365995a5c70cfb9209df1570"}, {file = "hiredis-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3020b60e3fc96d08c2a9b011f1c2e2a6bdcc09cb55df93c509b88be5cb791df"}, {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53d0f2c59bce399b8010a21bc779b4f8c32d0f582b2284ac8c98dc7578b27bc4"}, {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57c0d0c7e308ed5280a4900d4468bbfec51f0e1b4cde1deae7d4e639bc6b7766"}, {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d63318ca189fddc7e75f6a4af8eae9c0545863619fb38cfba5f43e81280b286"}, {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e741ffe4e2db78a1b9dd6e5d29678ce37fbaaf65dfe132e5b82a794413302ef1"}, {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb98038ccd368e0d88bd92ee575c58cfaf33e77f788c36b2a89a84ee1936dc6b"}, {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:eae62ed60d53b3561148bcd8c2383e430af38c0deab9f2dd15f8874888ffd26f"}, {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca33c175c1cf60222d9c6d01c38fc17ec3a484f32294af781de30226b003e00f"}, {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c5f6972d2bdee3cd301d5c5438e31195cf1cabf6fd9274491674d4ceb46914d"}, {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a6b54dabfaa5dbaa92f796f0c32819b4636e66aa8e9106c3d421624bd2a2d676"}, {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e96cd35df012a17c87ae276196ea8f215e77d6eeca90709eb03999e2d5e3fd8a"}, {file = "hiredis-2.3.2-cp39-cp39-win32.whl", hash = "sha256:63b99b5ea9fe4f21469fb06a16ca5244307678636f11917359e3223aaeca0b67"}, {file = "hiredis-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a50c8af811b35b8a43b1590cf890b61ff2233225257a3cad32f43b3ec7ff1b9f"}, {file = "hiredis-2.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e8bf4444b09419b77ce671088db9f875b26720b5872d97778e2545cd87dba4a"}, {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd42d0d45ea47a2f96babd82a659fbc60612ab9423a68e4a8191e538b85542a"}, {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80441b55edbef868e2563842f5030982b04349408396e5ac2b32025fb06b5212"}, {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec444ab8f27562a363672d6a7372bc0700a1bdc9764563c57c5f9efa0e592b5f"}, {file = "hiredis-2.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f9f606e810858207d4b4287b4ef0dc622c2aa469548bf02b59dcc616f134f811"}, {file = "hiredis-2.3.2-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c3dde4ca00fe9eee3b76209711f1941bb86db42b8a75d7f2249ff9dfc026ab0e"}, {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4dd676107a1d3c724a56a9d9db38166ad4cf44f924ee701414751bd18a784a0"}, {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce42649e2676ad783186264d5ffc788a7612ecd7f9effb62d51c30d413a3eefe"}, {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e3f8b1733078ac663dad57e20060e16389a60ab542f18a97931f3a2a2dd64a4"}, {file = "hiredis-2.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:532a84a82156a82529ec401d1c25d677c6543c791e54a263aa139541c363995f"}, {file = "hiredis-2.3.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d59f88c4daa36b8c38e59ac7bffed6f5d7f68eaccad471484bf587b28ccc478"}, {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91a14dd95e24dc078204b18b0199226ee44644974c645dc54ee7b00c3157330"}, {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb777a38797c8c7df0444533119570be18d1a4ce5478dffc00c875684df7bfcb"}, {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d47c915897a99d0d34a39fad4be97b4b709ab3d0d3b779ebccf2b6024a8c681e"}, {file = "hiredis-2.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:333b5e04866758b11bda5f5315b4e671d15755fc6ed3b7969721bc6311d0ee36"}, {file = "hiredis-2.3.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8937f1100435698c18e4da086968c4b5d70e86ea718376f833475ab3277c9aa"}, {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa45f7d771094b8145af10db74704ab0f698adb682fbf3721d8090f90e42cc49"}, {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d5ebc93c39aed4b5bc769f8ce0819bc50e74bb95d57a35f838f1c4378978e0"}, {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a797d8c7df9944314d309b0d9e1b354e2fa4430a05bb7604da13b6ad291bf959"}, {file = "hiredis-2.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e15a408f71a6c8c87b364f1f15a6cd9c1baca12bbc47a326ac8ab99ec7ad3c64"}, {file = "hiredis-2.3.2.tar.gz", hash = "sha256:733e2456b68f3f126ddaf2cd500a33b25146c3676b97ea843665717bda0c5d43"}, ] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "7.1.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "iron-core" version = "1.2.1" description = "Universal classes and methods for Iron.io API wrappers to build on." optional = true python-versions = "*" files = [ {file = "iron-core-1.2.1.tar.gz", hash = "sha256:b7190b86afbe5470da8389fbb412c0fa0529c87f85261736a87e5983ab6f1b95"}, {file = "iron_core-1.2.1-py3-none-any.whl", hash = "sha256:fd5ed3f9b441b9256bae1f829ae6f1da73fae62872aef395336f7d0ac2f041f7"}, ] [package.dependencies] python-dateutil = "*" requests = ">=1.1.0" [[package]] name = "iron-mq" version = "0.9" description = "Client library for IronMQ, a message queue in the cloud" optional = true python-versions = "*" files = [ {file = "iron-mq-0.9.tar.gz", hash = "sha256:c90441d872d9c08968343810a2ad1cca1664d80fd2ad3a3a2dbec57b7b38ecfa"}, ] [package.dependencies] iron_core = "*" [[package]] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "jinxed" version = "1.2.1" description = "Jinxed Terminal Library" optional = true python-versions = "*" files = [ {file = "jinxed-1.2.1-py2.py3-none-any.whl", hash = "sha256:37422659c4925969c66148c5e64979f553386a4226b9484d910d3094ced37d30"}, {file = "jinxed-1.2.1.tar.gz", hash = "sha256:30c3f861b73279fea1ed928cfd4dfb1f273e16cd62c8a32acfac362da0f78f3f"}, ] [package.dependencies] ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" optional = true python-versions = ">=3.7" files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] [[package]] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "packaging" version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "psutil" version = "5.9.8" description = "Cross-platform lib for process and system monitoring in Python." optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, ] [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymongo" version = "4.7.2" description = "Python driver for MongoDB " optional = true python-versions = ">=3.7" files = [ {file = "pymongo-4.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:268d8578c0500012140c5460755ea405cbfe541ef47c81efa9d6744f0f99aeca"}, {file = "pymongo-4.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:827611beb6c483260d520cfa6a49662d980dfa5368a04296f65fa39e78fccea7"}, {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a754e366c404d19ff3f077ddeed64be31e0bb515e04f502bf11987f1baa55a16"}, {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44efab10d9a3db920530f7bcb26af8f408b7273d2f0214081d3891979726328"}, {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35b3f0c7d49724859d4df5f0445818d525824a6cd55074c42573d9b50764df67"}, {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e37faf298a37ffb3e0809e77fbbb0a32b6a2d18a83c59cfc2a7b794ea1136b0"}, {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1bcd58669e56c08f1e72c5758868b5df169fe267501c949ee83c418e9df9155"}, {file = "pymongo-4.7.2-cp310-cp310-win32.whl", hash = "sha256:c72d16fede22efe7cdd1f422e8da15760e9498024040429362886f946c10fe95"}, {file = "pymongo-4.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:12d1fef77d25640cb78893d07ff7d2fac4c4461d8eec45bd3b9ad491a1115d6e"}, {file = "pymongo-4.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc5af24fcf5fc6f7f40d65446400d45dd12bea933d0299dc9e90c5b22197f1e9"}, {file = "pymongo-4.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:730778b6f0964b164c187289f906bbc84cb0524df285b7a85aa355bbec43eb21"}, {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47a1a4832ef2f4346dcd1a10a36ade7367ad6905929ddb476459abb4fd1b98cb"}, {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6eab12c6385526d386543d6823b07187fefba028f0da216506e00f0e1855119"}, {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37e9ea81fa59ee9274457ed7d59b6c27f6f2a5fe8e26f184ecf58ea52a019cb8"}, {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e9d9d2c0aae73aa4369bd373ac2ac59f02c46d4e56c4b6d6e250cfe85f76802"}, {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6e00a79dff22c9a72212ad82021b54bdb3b85f38a85f4fc466bde581d7d17a"}, {file = "pymongo-4.7.2-cp311-cp311-win32.whl", hash = "sha256:02efd1bb3397e24ef2af45923888b41a378ce00cb3a4259c5f4fc3c70497a22f"}, {file = "pymongo-4.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:87bb453ac3eb44db95cb6d5a616fbc906c1c00661eec7f55696253a6245beb8a"}, {file = "pymongo-4.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:12c466e02133b7f8f4ff1045c6b5916215c5f7923bc83fd6e28e290cba18f9f6"}, {file = "pymongo-4.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f91073049c43d14e66696970dd708d319b86ee57ef9af359294eee072abaac79"}, {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87032f818bf5052ab742812c715eff896621385c43f8f97cdd37d15b5d394e95"}, {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a87eef394039765679f75c6a47455a4030870341cb76eafc349c5944408c882"}, {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d275596f840018858757561840767b39272ac96436fcb54f5cac6d245393fd97"}, {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82102e353be13f1a6769660dd88115b1da382447672ba1c2662a0fbe3df1d861"}, {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194065c9d445017b3c82fb85f89aa2055464a080bde604010dc8eb932a6b3c95"}, {file = "pymongo-4.7.2-cp312-cp312-win32.whl", hash = "sha256:db4380d1e69fdad1044a4b8f3bb105200542c49a0dde93452d938ff9db1d6d29"}, {file = "pymongo-4.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:fadc6e8db7707c861ebe25b13ad6aca19ea4d2c56bf04a26691f46c23dadf6e4"}, {file = "pymongo-4.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2cb77d09bd012cb4b30636e7e38d00b5f9be5eb521c364bde66490c45ee6c4b4"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bf8b706946952acdea0fe478f8e44f1ed101c4b87f046859e6c3abe6c0a9f4"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcf337d1b252405779d9c79978d6ca15eab3cdaa2f44c100a79221bddad97c8a"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ffd1519edbe311df73c74ec338de7d294af535b2748191c866ea3a7c484cd15"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d59776f435564159196d971aa89422ead878174aff8fe18e06d9a0bc6d648c"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:347c49cf7f0ba49ea87c1a5a1984187ecc5516b7c753f31938bf7b37462824fd"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84bc00200c3cbb6c98a2bb964c9e8284b641e4a33cf10c802390552575ee21de"}, {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fcaf8c911cb29316a02356f89dbc0e0dfcc6a712ace217b6b543805690d2aefd"}, {file = "pymongo-4.7.2-cp37-cp37m-win32.whl", hash = "sha256:b48a5650ee5320d59f6d570bd99a8d5c58ac6f297a4e9090535f6561469ac32e"}, {file = "pymongo-4.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5239ef7e749f1326ea7564428bf861d5250aa39d7f26d612741b1b1273227062"}, {file = "pymongo-4.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2dcf608d35644e8d276d61bf40a93339d8d66a0e5f3e3f75b2c155a421a1b71"}, {file = "pymongo-4.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25eeb2c18ede63891cbd617943dd9e6b9cbccc54f276e0b2e693a0cc40f243c5"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9349f0bb17a31371d4cacb64b306e4ca90413a3ad1fffe73ac7cd495570d94b5"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffd4d7cb2e6c6e100e2b39606d38a9ffc934e18593dc9bb326196afc7d93ce3d"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a8bd37f5dabc86efceb8d8cbff5969256523d42d08088f098753dba15f3b37a"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c78f156edc59b905c80c9003e022e1a764c54fd40ac4fea05b0764f829790e2"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d892fb91e81cccb83f507cdb2ea0aa026ec3ced7f12a1d60f6a5bf0f20f9c1f"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87832d6076c2c82f42870157414fd876facbb6554d2faf271ffe7f8f30ce7bed"}, {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ce1a374ea0e49808e0380ffc64284c0ce0f12bd21042b4bef1af3eb7bdf49054"}, {file = "pymongo-4.7.2-cp38-cp38-win32.whl", hash = "sha256:eb0642e5f0dd7e86bb358749cc278e70b911e617f519989d346f742dc9520dfb"}, {file = "pymongo-4.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:4bdb5ffe1cd3728c9479671a067ef44dacafc3743741d4dc700c377c4231356f"}, {file = "pymongo-4.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:743552033c63f0afdb56b9189ab04b5c1dbffd7310cf7156ab98eebcecf24621"}, {file = "pymongo-4.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5239776633f7578b81207e5646245415a5a95f6ae5ef5dff8e7c2357e6264bfc"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727ad07952c155cd20045f2ce91143c7dc4fb01a5b4e8012905a89a7da554b0c"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9385654f01a90f73827af4db90c290a1519f7d9102ba43286e187b373e9a78e9"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d833651f1ba938bb7501f13e326b96cfbb7d98867b2d545ca6d69c7664903e0"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf17ea9cea14d59b0527403dd7106362917ced7c4ec936c4ba22bd36c912c8e0"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cecd2df037249d1c74f0af86fb5b766104a5012becac6ff63d85d1de53ba8b98"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65b4c00dedbd333698b83cd2095a639a6f0d7c4e2a617988f6c65fb46711f028"}, {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d9b6cbc037108ff1a0a867e7670d8513c37f9bcd9ee3d2464411bfabf70ca002"}, {file = "pymongo-4.7.2-cp39-cp39-win32.whl", hash = "sha256:cf28430ec1924af1bffed37b69a812339084697fd3f3e781074a0148e6475803"}, {file = "pymongo-4.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:e004527ea42a6b99a8b8d5b42b42762c3bdf80f88fbdb5c3a9d47f3808495b86"}, {file = "pymongo-4.7.2.tar.gz", hash = "sha256:9024e1661c6e40acf468177bf90ce924d1bc681d2b244adda3ed7b2f4c4d17d7"}, ] [package.dependencies] dnspython = ">=1.16.0,<3.0.0" [package.extras] aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"] gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] test = ["pytest (>=7)"] zstd = ["zstandard"] [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-django" version = "4.8.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" files = [ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "pytz" version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" optional = true python-versions = ">=3.7" files = [ {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, ] [package.dependencies] async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rollbar" version = "0.14.7" description = "Easy and powerful exception tracking with Rollbar. Send messages and exceptions with arbitrary context, get back aggregates, and debug production issues quickly." optional = true python-versions = "*" files = [ {file = "rollbar-0.14.7.tar.gz", hash = "sha256:ee2dd1a2b512f93e9c0f26c8cf6bb28c4f06ae7e0c33a60e39b49337e0d92d57"}, ] [package.dependencies] requests = ">=0.12.1" six = ">=1.9.0" [[package]] name = "ruff" version = "0.4.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, ] [[package]] name = "s3transfer" version = "0.10.1" description = "An Amazon S3 Transfer Manager" optional = true python-versions = ">= 3.8" files = [ {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, ] [package.dependencies] botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "sentry-sdk" version = "2.1.1" description = "Python client for Sentry (https://sentry.io)" optional = true python-versions = ">=3.6" files = [ {file = "sentry_sdk-2.1.1-py2.py3-none-any.whl", hash = "sha256:99aeb78fb76771513bd3b2829d12613130152620768d00cd3e45ac00cb17950f"}, {file = "sentry_sdk-2.1.1.tar.gz", hash = "sha256:95d8c0bb41c8b0bc37ab202c2c4a295bb84398ee05f4cdce55051cd75b926ec1"}, ] [package.dependencies] certifi = "*" urllib3 = ">=1.26.11" [package.extras] aiohttp = ["aiohttp (>=3.5)"] anthropic = ["anthropic (>=0.16)"] arq = ["arq (>=0.23)"] asyncpg = ["asyncpg (>=0.23)"] beam = ["apache-beam (>=2.12)"] bottle = ["bottle (>=0.12.13)"] celery = ["celery (>=3)"] celery-redbeat = ["celery-redbeat (>=2)"] chalice = ["chalice (>=1.16.0)"] clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] django = ["django (>=1.8)"] falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] grpcio = ["grpcio (>=1.21.1)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] huggingface-hub = ["huggingface-hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] pure-eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] tornado = ["tornado (>=5)"] [[package]] name = "setproctitle" version = "1.3.3" description = "A Python module to customize the process title" optional = true python-versions = ">=3.7" files = [ {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:897a73208da48db41e687225f355ce993167079eda1260ba5e13c4e53be7f754"}, {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c331e91a14ba4076f88c29c777ad6b58639530ed5b24b5564b5ed2fd7a95452"}, {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbd6c7de0771c84b4aa30e70b409565eb1fc13627a723ca6be774ed6b9d9fa3"}, {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c05ac48ef16ee013b8a326c63e4610e2430dbec037ec5c5b58fcced550382b74"}, {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1342f4fdb37f89d3e3c1c0a59d6ddbedbde838fff5c51178a7982993d238fe4f"}, {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc74e84fdfa96821580fb5e9c0b0777c1c4779434ce16d3d62a9c4d8c710df39"}, {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9617b676b95adb412bb69645d5b077d664b6882bb0d37bfdafbbb1b999568d85"}, {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6a249415f5bb88b5e9e8c4db47f609e0bf0e20a75e8d744ea787f3092ba1f2d0"}, {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:38da436a0aaace9add67b999eb6abe4b84397edf4a78ec28f264e5b4c9d53cd5"}, {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:da0d57edd4c95bf221b2ebbaa061e65b1788f1544977288bdf95831b6e44e44d"}, {file = "setproctitle-1.3.3-cp310-cp310-win32.whl", hash = "sha256:a1fcac43918b836ace25f69b1dca8c9395253ad8152b625064415b1d2f9be4fb"}, {file = "setproctitle-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:200620c3b15388d7f3f97e0ae26599c0c378fdf07ae9ac5a13616e933cbd2086"}, {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:334f7ed39895d692f753a443102dd5fed180c571eb6a48b2a5b7f5b3564908c8"}, {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:950f6476d56ff7817a8fed4ab207727fc5260af83481b2a4b125f32844df513a"}, {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:195c961f54a09eb2acabbfc90c413955cf16c6e2f8caa2adbf2237d1019c7dd8"}, {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f05e66746bf9fe6a3397ec246fe481096664a9c97eb3fea6004735a4daf867fd"}, {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5901a31012a40ec913265b64e48c2a4059278d9f4e6be628441482dd13fb8b5"}, {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64286f8a995f2cd934082b398fc63fca7d5ffe31f0e27e75b3ca6b4efda4e353"}, {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:184239903bbc6b813b1a8fc86394dc6ca7d20e2ebe6f69f716bec301e4b0199d"}, {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:664698ae0013f986118064b6676d7dcd28fefd0d7d5a5ae9497cbc10cba48fa5"}, {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e5119a211c2e98ff18b9908ba62a3bd0e3fabb02a29277a7232a6fb4b2560aa0"}, {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:417de6b2e214e837827067048f61841f5d7fc27926f2e43954567094051aff18"}, {file = "setproctitle-1.3.3-cp311-cp311-win32.whl", hash = "sha256:6a143b31d758296dc2f440175f6c8e0b5301ced3b0f477b84ca43cdcf7f2f476"}, {file = "setproctitle-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a680d62c399fa4b44899094027ec9a1bdaf6f31c650e44183b50d4c4d0ccc085"}, {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d4460795a8a7a391e3567b902ec5bdf6c60a47d791c3b1d27080fc203d11c9dc"}, {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bdfd7254745bb737ca1384dee57e6523651892f0ea2a7344490e9caefcc35e64"}, {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477d3da48e216d7fc04bddab67b0dcde633e19f484a146fd2a34bb0e9dbb4a1e"}, {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab2900d111e93aff5df9fddc64cf51ca4ef2c9f98702ce26524f1acc5a786ae7"}, {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088b9efc62d5aa5d6edf6cba1cf0c81f4488b5ce1c0342a8b67ae39d64001120"}, {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d50252377db62d6a0bb82cc898089916457f2db2041e1d03ce7fadd4a07381"}, {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:87e668f9561fd3a457ba189edfc9e37709261287b52293c115ae3487a24b92f6"}, {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:287490eb90e7a0ddd22e74c89a92cc922389daa95babc833c08cf80c84c4df0a"}, {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe1c49486109f72d502f8be569972e27f385fe632bd8895f4730df3c87d5ac8"}, {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4a6ba2494a6449b1f477bd3e67935c2b7b0274f2f6dcd0f7c6aceae10c6c6ba3"}, {file = "setproctitle-1.3.3-cp312-cp312-win32.whl", hash = "sha256:2df2b67e4b1d7498632e18c56722851ba4db5d6a0c91aaf0fd395111e51cdcf4"}, {file = "setproctitle-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f38d48abc121263f3b62943f84cbaede05749047e428409c2c199664feb6abc7"}, {file = "setproctitle-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:816330675e3504ae4d9a2185c46b573105d2310c20b19ea2b4596a9460a4f674"}, {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f960bc22d8d8e4ac886d1e2e21ccbd283adcf3c43136161c1ba0fa509088e0"}, {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e6e7adff74796ef12753ff399491b8827f84f6c77659d71bd0b35870a17d8f"}, {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53bc0d2358507596c22b02db079618451f3bd720755d88e3cccd840bafb4c41c"}, {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6d20f9541f5f6ac63df553b6d7a04f313947f550eab6a61aa758b45f0d5657"}, {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c1c84beab776b0becaa368254801e57692ed749d935469ac10e2b9b825dbdd8e"}, {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:507e8dc2891021350eaea40a44ddd887c9f006e6b599af8d64a505c0f718f170"}, {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b1067647ac7aba0b44b591936118a22847bda3c507b0a42d74272256a7a798e9"}, {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e71f6365744bf53714e8bd2522b3c9c1d83f52ffa6324bd7cbb4da707312cd8"}, {file = "setproctitle-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:7f1d36a1e15a46e8ede4e953abb104fdbc0845a266ec0e99cc0492a4364f8c44"}, {file = "setproctitle-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9a402881ec269d0cc9c354b149fc29f9ec1a1939a777f1c858cdb09c7a261df"}, {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ff814dea1e5c492a4980e3e7d094286077054e7ea116cbeda138819db194b2cd"}, {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:accb66d7b3ccb00d5cd11d8c6e07055a4568a24c95cf86109894dcc0c134cc89"}, {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554eae5a5b28f02705b83a230e9d163d645c9a08914c0ad921df363a07cf39b1"}, {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a911b26264dbe9e8066c7531c0591cfab27b464459c74385b276fe487ca91c12"}, {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2982efe7640c4835f7355fdb4da313ad37fb3b40f5c69069912f8048f77b28c8"}, {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df3f4274b80709d8bcab2f9a862973d453b308b97a0b423a501bcd93582852e3"}, {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:af2c67ae4c795d1674a8d3ac1988676fa306bcfa1e23fddb5e0bd5f5635309ca"}, {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af4061f67fd7ec01624c5e3c21f6b7af2ef0e6bab7fbb43f209e6506c9ce0092"}, {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37a62cbe16d4c6294e84670b59cf7adcc73faafe6af07f8cb9adaf1f0e775b19"}, {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a83ca086fbb017f0d87f240a8f9bbcf0809f3b754ee01cec928fff926542c450"}, {file = "setproctitle-1.3.3-cp38-cp38-win32.whl", hash = "sha256:059f4ce86f8cc92e5860abfc43a1dceb21137b26a02373618d88f6b4b86ba9b2"}, {file = "setproctitle-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ab92e51cd4a218208efee4c6d37db7368fdf182f6e7ff148fb295ecddf264287"}, {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c7951820b77abe03d88b114b998867c0f99da03859e5ab2623d94690848d3e45"}, {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc94cf128676e8fac6503b37763adb378e2b6be1249d207630f83fc325d9b11"}, {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5d9027eeda64d353cf21a3ceb74bb1760bd534526c9214e19f052424b37e42"}, {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e4a8104db15d3462e29d9946f26bed817a5b1d7a47eabca2d9dc2b995991503"}, {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32c41ace41f344d317399efff4cffb133e709cec2ef09c99e7a13e9f3b9483c"}, {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf16381c7bf7f963b58fb4daaa65684e10966ee14d26f5cc90f07049bfd8c1e"}, {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e18b7bd0898398cc97ce2dfc83bb192a13a087ef6b2d5a8a36460311cb09e775"}, {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69d565d20efe527bd8a9b92e7f299ae5e73b6c0470f3719bd66f3cd821e0d5bd"}, {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ddedd300cd690a3b06e7eac90ed4452348b1348635777ce23d460d913b5b63c3"}, {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:415bfcfd01d1fbf5cbd75004599ef167a533395955305f42220a585f64036081"}, {file = "setproctitle-1.3.3-cp39-cp39-win32.whl", hash = "sha256:21112fcd2195d48f25760f0eafa7a76510871bbb3b750219310cf88b04456ae3"}, {file = "setproctitle-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:5a740f05d0968a5a17da3d676ce6afefebeeeb5ce137510901bf6306ba8ee002"}, {file = "setproctitle-1.3.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6b9e62ddb3db4b5205c0321dd69a406d8af9ee1693529d144e86bd43bcb4b6c0"}, {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e3b99b338598de0bd6b2643bf8c343cf5ff70db3627af3ca427a5e1a1a90dd9"}, {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ae9a02766dad331deb06855fb7a6ca15daea333b3967e214de12cfae8f0ef5"}, {file = "setproctitle-1.3.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:200ede6fd11233085ba9b764eb055a2a191fb4ffb950c68675ac53c874c22e20"}, {file = "setproctitle-1.3.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0d3a953c50776751e80fe755a380a64cb14d61e8762bd43041ab3f8cc436092f"}, {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e08e232b78ba3ac6bc0d23ce9e2bee8fad2be391b7e2da834fc9a45129eb87"}, {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1da82c3e11284da4fcbf54957dafbf0655d2389cd3d54e4eaba636faf6d117a"}, {file = "setproctitle-1.3.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:aeaa71fb9568ebe9b911ddb490c644fbd2006e8c940f21cb9a1e9425bd709574"}, {file = "setproctitle-1.3.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:59335d000c6250c35989394661eb6287187854e94ac79ea22315469ee4f4c244"}, {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3ba57029c9c50ecaf0c92bb127224cc2ea9fda057b5d99d3f348c9ec2855ad3"}, {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d876d355c53d975c2ef9c4f2487c8f83dad6aeaaee1b6571453cb0ee992f55f6"}, {file = "setproctitle-1.3.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:224602f0939e6fb9d5dd881be1229d485f3257b540f8a900d4271a2c2aa4e5f4"}, {file = "setproctitle-1.3.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d7f27e0268af2d7503386e0e6be87fb9b6657afd96f5726b733837121146750d"}, {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5e7266498cd31a4572378c61920af9f6b4676a73c299fce8ba93afd694f8ae7"}, {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33c5609ad51cd99d388e55651b19148ea99727516132fb44680e1f28dd0d1de9"}, {file = "setproctitle-1.3.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:eae8988e78192fd1a3245a6f4f382390b61bce6cfcc93f3809726e4c885fa68d"}, {file = "setproctitle-1.3.3.tar.gz", hash = "sha256:c913e151e7ea01567837ff037a23ca8740192880198b7fbb90b16d181607caae"}, ] [package.extras] test = ["pytest"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "4.5.0" description = "Python documentation generator" optional = false python-versions = ">=3.6" files = [ {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.18" imagesize = "*" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.8" files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.8" files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sqlparse" version = "0.5.0" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, ] [package.extras] dev = ["build", "hatch"] doc = ["sphinx"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "tzdata" version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] name = "urllib3" version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "urllib3" version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = true python-versions = "*" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] name = "zipp" version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] build-backend = [] requires = [] rollbar = ["django-q-rollbar"] sentry = ["django-q-sentry"] testing = ["blessed", "boto3", "croniter", "django-redis", "hiredis", "iron-mq", "psutil", "pymongo", "redis", "setproctitle"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4" content-hash = "4d85950a60ac6971054cec256bba60f5f9273cccdce89cb219824220556abebb" django-q2-1.7.4/pyproject.toml000066400000000000000000000054521471170400300162240ustar00rootroot00000000000000[build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "django-q2" version = "1.7.4" packages = [ { include = "django_q" }, ] description = "A multiprocessing distributed task queue for Django" authors = ["Stan Triepels ", "Ilan Steemers "] maintainers = ["Stan Triepels "] license = "MIT" readme = 'README.rst' repository = "https://github.com/GDay/django-q2" homepage = "https://django-q2.readthedocs.org" documentation = "https://django-q2.readthedocs.org" keywords = ["django", "distributed", "multiprocessing", "queue", "scheduler"] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Operating System :: MacOS', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Internet :: WWW/HTTP', 'Topic :: System :: Distributed Computing', 'Topic :: Software Development :: Libraries :: Python Modules', ] include = ['CHANGELOG.md'] [tool.poetry.plugins] # Optional super table [tool.poetry.plugins."djangoq.errorreporters"] "rollbar" = "django_q_rollbar:Rollbar" "sentry" = "django_q_sentry:Sentry" [tool.poetry.dependencies] python = ">=3.8,<4" django = ">=4.2, <6" django-picklefield = "^3.1" blessed = { version = "^1.19.1", optional = true } hiredis = { version = "^2.0.0", optional = true } psutil = { version = "^5.9.2", optional = true } django-redis = { version = "^5.2.0", optional = true } iron-mq = { version = "^0.9", optional = true } boto3 = { version = "^1.24.92", optional = true } pymongo = { version = "^4.2.0", optional = true } croniter = { version = "^2.0.1", optional = true } django-q-rollbar = {version = ">=0.1", optional = true} django-q-sentry = {version = ">=0.1", optional = true} redis = {version = "^4.3.4", optional = true} setproctitle = {version = "^1.3.2", optional = true} importlib-metadata = {version = ">=3.6", python = "<3.10"} [tool.poetry.dev-dependencies] pytest = "^7.1.3" pytest-django = "^4.5.2" Sphinx = "^4.0.2" pytest-cov = "^4.0.0" ruff = "^0.4.4" [tool.poetry.extras] requires = ["poetry_core"] build-backend = ["poetry.core.masonry.api"] testing = ["django-redis", "croniter", "hiredis", "psutil", "iron-mq", "boto3", "pymongo", "blessed", "redis", "setproctitle"] rollbar = ["django-q-rollbar"] sentry = ["django-q-sentry"] [tool.isort] profile = "black" multi_line_output = 3 django-q2-1.7.4/pytest.ini000066400000000000000000000000671471170400300153360ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE=django_q.tests.settingsdjango-q2-1.7.4/requirements.txt000066400000000000000000000065021471170400300165710ustar00rootroot00000000000000asgiref==3.8.1 ; python_version >= "3.8" and python_version < "4" \ --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \ --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590 backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" \ --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \ --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \ --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \ --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \ --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \ --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \ --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \ --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \ --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \ --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \ --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \ --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \ --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \ --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \ --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \ --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2 django-picklefield==3.2 ; python_version >= "3.8" and python_version < "4" \ --hash=sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d \ --hash=sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c django==4.2.11 ; python_version >= "3.8" and python_version < "4" \ --hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \ --hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3 importlib-metadata==7.1.0 ; python_version >= "3.8" and python_version < "3.10" \ --hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \ --hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2 sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4" \ --hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \ --hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663 typing-extensions==4.11.0 ; python_version >= "3.8" and python_version < "3.11" \ --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a tzdata==2024.1 ; python_version >= "3.8" and python_version < "4" and sys_platform == "win32" \ --hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \ --hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252 zipp==3.18.1 ; python_version >= "3.8" and python_version < "3.10" \ --hash=sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b \ --hash=sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715 django-q2-1.7.4/ruff.toml000066400000000000000000000000331471170400300151350ustar00rootroot00000000000000lint.extend-select = ["I"] django-q2-1.7.4/test-services-docker-compose.yaml000066400000000000000000000023171471170400300217010ustar00rootroot00000000000000services: redis: image: redis:latest expose: - '6379/tcp' networks: - main mongo: image: mongo:6 expose: - '27017/tcp' networks: - main aws: container_name: aws image: localstack/localstack:3.4.0 ports: - "127.0.0.1:4566:4566" # LocalStack Gateway - "127.0.0.1:4510-4559:4510-4559" # External services port range environment: AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2} DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2} SQS_ENDPOINT_STRATEGY: path SERVICES: sqs LOCALSTACK_HOST: aws DEBUG: 1 LS_LOG: trace volumes: - ./containers/localstack:/etc/localstack/init/ready.d networks: - main django-q2: build: dockerfile: ./Dockerfile.dev context: . environment: AWS_ENDPOINT_URL: http://aws:4566 AWS_REGION: ${AWS_REGION:-us-west-2} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test} AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2} volumes: - .:/app depends_on: - redis - mongo - aws networks: - main networks: main: django-q2-1.7.4/tox.ini000066400000000000000000000000341471170400300146120ustar00rootroot00000000000000[flake8] max-line-length=88 django-q2-1.7.4/web-docker-compose.yaml000066400000000000000000000005511471170400300176540ustar00rootroot00000000000000services: web: restart: always command: bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" ports: - "127.0.0.1:8000:8000" build: . volumes: - .:/app django-q: restart: always command: bash -c "python manage.py migrate && python manage.py qcluster" build: . volumes: - .:/app