From 97cf55310750e1ccbb0ef99fa9ee749c330e1ea5 Mon Sep 17 00:00:00 2001 From: Farruh Sheripov Date: Thu, 14 Sep 2023 19:09:51 +0200 Subject: [PATCH] MP-432 Create Django library for Google Structured Logger (GSL) --- .github/workflows/release.yml | 38 ++ .gitignore | 10 + .pre-commit-config.yaml | 42 ++ LICENSE | 7 + MANIFEST.in | 7 + README.md | 88 ++++ django_google_structured_logger/__init__.py | 0 django_google_structured_logger/apps.py | 6 + django_google_structured_logger/formatter.py | 57 +++ django_google_structured_logger/middleware.py | 304 ++++++++++++ django_google_structured_logger/settings.py | 36 ++ django_google_structured_logger/storages.py | 19 + poetry.lock | 462 ++++++++++++++++++ pyproject.toml | 21 + setup.cfg | 44 ++ setup.py | 6 + 16 files changed, 1147 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 django_google_structured_logger/__init__.py create mode 100644 django_google_structured_logger/apps.py create mode 100644 django_google_structured_logger/formatter.py create mode 100644 django_google_structured_logger/middleware.py create mode 100644 django_google_structured_logger/settings.py create mode 100644 django_google_structured_logger/storages.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a40859b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + if: github.repository == 'muehlemann-popp/django-google-structured-logger' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools twine wheel + + - name: Build package + run: | + python setup.py --version + python setup.py sdist --format=gztar bdist_wheel + twine check dist/* + + - name: Publish package + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..830b990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/*.egg-info +.eggs +/__pycache__ +.pytest_cache +*.pyc +/reports +/.coverage +/build +.idea +dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100755 index 0000000..cce79e7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +exclude: docs|node_modules|migrations|.tox +default_stages: [commit] +fail_fast: true + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + exclude: static + - id: end-of-file-fixer + exclude: static + - id: pretty-format-json + args: [--autofix] + +- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.10.0 + hooks: + - id: pretty-format-yaml + args: [--autofix, --indent, '2'] + +- repo: https://github.com/timothycrosley/isort + rev: 5.12.0 + hooks: + - id: isort + +- repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + +- repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-isort] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.5.1 + hooks: + - id: mypy + args: [--install-types, --non-interactive] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2c33d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Mühlemann & Popp Online Media AG + +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2efba00 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include README.md +include LICENSE +recursive-include docs * + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] +recursive-exclude * *.orig diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3ce269 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +## Django Google Structured Logger + +**Django Google Structured Logger** is a Django middleware designed to capture and log details from incoming requests and outgoing responses. It offers features to mask sensitive data, set default fields for Google Cloud Logging, and structure logs in a detailed and organized manner. + +## Contents +* [Features](#features) +* [Usage](#usage) +* [Key Components](#key-components) +* [Settings](#settings) +* [Conclusion](#conclusion) + +### Features: + +1. **Detailed Logging**: Logs both requests and responses with meticulous details. +2. **Sensitive Data Masking**: Masks sensitive information using customizable regex patterns. +3. **Google Cloud Logging Support**: Formats logs to match Google Cloud Logging standards. +4. **Configurable Settings**: Customize log behavior through Django settings. + +### Usage: + +1. Add `GoogleFormatter` to your Django's `LOGGING` setting. + Example: + ```python + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json": { + "()": "django_google_structured_logger.formatter.GoogleFormatter", + }, + }, + "handlers": { + "google-json-handler": { + "class": "logging.StreamHandler", + "formatter": "json", + }, + }, + "root": { + "handlers": ["google-json-handler"], + "level": logging.INFO, + } + } + ``` +2. Add `SetRequestToLoggerMiddleware` to your Django's `MIDDLEWARE` setting. + Example: + ```python + MIDDLEWARE = [ + ... + "django_google_structured_logger.middleware.SetRequestToLoggerMiddleware", + ] + ``` +3. Ensure your Django project has the necessary configurations in the `settings.py`. + +### Key Components: + +#### 1. middleware.py + +- **SetRequestToLoggerMiddleware**: This class contains methods to process incoming requests and outgoing responses and then log them. It supports features like abridging lengthy data and masking sensitive information. + +#### 2. formatter.py + +- **GoogleFormatter**: Extends `jsonlogger.JsonFormatter` to format logs specifically for Google Cloud Logging. It sets default fields such as severity, labels, operation, and source location based on Google's logging standards. + +#### 3. settings.py + +- Provides a list of default sensitive keys for data masking. +- Allows customization of logging behavior with options to specify maximum string length, excluded endpoints, sensitive keys, and more. + +### Settings: + +These are the settings that can be customized for the middleware: + +- **LOG_MAX_STR_LEN**: Maximum string length before data is abridged. Default is 200. +- **LOG_MAX_LIST_LEN**: Maximum list length before data is abridged. Default is 10. +- **LOG_EXCLUDED_ENDPOINTS**: List of endpoints to exclude from logging. Default is an empty list. +- **LOG_SENSITIVE_KEYS**: Regex patterns for keys which contain sensitive data. Defaults provided. +- **LOG_MASK_STYLE**: Style for masking sensitive data. Default is "partially". +- **LOG_MASK_CUSTOM_STYLE**: Custom style for masking if `LOG_MASK_STYLE` is set to "custom". Default is just the data itself. +- **LOG_MIDDLEWARE_ENABLED**: Enable or disable the logging middleware. Default is True. +- **LOG_EXCLUDED_HEADERS**: List of request headers to exclude from logging. Default is ["Authorization"]. +- **LOG_USER_ID_FIELD**: Field name for user ID. Default is "id". +- **LOG_USER_EMAIL_FIELD**: Field name for user email. Default is "email". + +### Conclusion: + +**SetRequestToLoggerMiddleware** is a comprehensive solution for those seeking enhanced logging capabilities in their Django projects, with particular attention to sensitive data protection and compatibility with Google Cloud Logging. + +To get started, integrate the provided middleware, formatter, and settings into your Django project, customize as needed, and enjoy advanced logging capabilities! diff --git a/django_google_structured_logger/__init__.py b/django_google_structured_logger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_google_structured_logger/apps.py b/django_google_structured_logger/apps.py new file mode 100644 index 0000000..eaf8ab0 --- /dev/null +++ b/django_google_structured_logger/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig # type: ignore + + +class DjangoMaterializedViewAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "django_google_structured_logger" diff --git a/django_google_structured_logger/formatter.py b/django_google_structured_logger/formatter.py new file mode 100644 index 0000000..d02760a --- /dev/null +++ b/django_google_structured_logger/formatter.py @@ -0,0 +1,57 @@ +from pythonjsonlogger import jsonlogger # type: ignore + +from .storages import RequestStorage, get_current_request + + +class GoogleFormatter(jsonlogger.JsonFormatter): + google_source_location_field = "logging.googleapis.com/sourceLocation" + google_operation_field = "logging.googleapis.com/operation" + google_labels_field = "logging.googleapis.com/labels" + + def add_fields(self, log_record: dict, record, message_dict: dict): + """Set Google default fields + + List of Google supported fields: + https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + + List of associated JSON fields: + https://cloud.google.com/logging/docs/structured-logging#default-parsers + + This method sets these fields if present: + - severity + - labels + - operation + - sourceLocation + """ + super().add_fields(log_record, record, message_dict) + current_request: RequestStorage | None = get_current_request() + + log_record["severity"] = record.levelname + + log_record[self.google_labels_field] = { + "request_user_id": current_request.user_id() if current_request else None, + "request_user_email": current_request.user_email() + if current_request + else None, + **log_record.pop(self.google_labels_field, {}), + **log_record.pop("labels", {}), + } + self.stringify_values(log_record[self.google_labels_field]) + + log_record[self.google_operation_field] = { + "id": current_request.uuid if current_request else None, + "last": log_record.get("last_operation", False), + **log_record.pop(self.google_operation_field, {}), + **log_record.pop("operation", {}), + } + log_record[self.google_source_location_field] = { + "file": record.pathname, + "line": record.lineno, + "function": record.funcName, + "logger": record.name, + } + + @staticmethod + def stringify_values(dict_to_convert: dict): + for key in dict_to_convert: + dict_to_convert[key] = str(dict_to_convert[key]) diff --git a/django_google_structured_logger/middleware.py b/django_google_structured_logger/middleware.py new file mode 100644 index 0000000..f7f31ff --- /dev/null +++ b/django_google_structured_logger/middleware.py @@ -0,0 +1,304 @@ +import json +import logging +import re +import uuid +from copy import deepcopy +from typing import Any, OrderedDict + +from django.http import HttpRequest, HttpResponse # type: ignore + +from . import settings +from .storages import RequestStorage, _current_request + +logger = logging.getLogger(__name__) + + +class SetRequestToLoggerMiddleware: + """Middleware for logging requests and responses with sensitive data masked.""" + + def __init__(self, get_response): + # One-time configuration and initialization. + self.get_response = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + # Early exit if logging middleware is disabled + if not settings.LOG_MIDDLEWARE_ENABLED: + return self.get_response(request) + + # Code to be executed for each request before + # the view (and later middleware) are called. + + response = self.get_response(request) + # Code to be executed for each request/response after + # the view is called. + self.process_request(request) + self.process_response(request, response) + return response + + def process_request(self, request): + """ + Log necessary data from the incoming request. + + Example: + Input: Django request object + Output: Logs the necessary details of the request + """ + if self._is_ignored(request): + return request + + user_id = lambda: self._get_user_id(request) # noqa + user_email = lambda: self._get_user_email(request) # noqa + request_uuid = str(uuid.uuid4()) + + try: + _current_request.set( + RequestStorage( + user_id=user_id, user_email=user_email, uuid=request_uuid + ) + ) + + request_data = self._get_request_data(request) + request_method = request_data["request"]["method"] + request_path = request_data["request"]["path"] + + logger.info( + f"Request {request_method} {request_path}", + extra=request_data, + ) + except Exception as exc: + logger.exception(exc) + + def process_response(self, request, response): + """ + Log necessary data from the outgoing response. + + Example: + Input: Django request object, Django response object + Output: Logs the necessary details of the response + """ + if self._is_ignored(request): + return response + + try: + response_data = self._abridge(getattr(response, "data", None)) + response_status_code = getattr(response, "status_code", "Unknown STATUS") + response_headers = self._exclude_keys( + getattr(response, "headers", None), settings.LOG_EXCLUDED_HEADERS + ) + + data = { + "response": { + "headers": response_headers, + "data": response_data, + "status_code": response_status_code, + }, + "last_operation": True, + } + + log_message = ( + f"Response {request.method} {request.path} > {response_status_code}" + ) + logger_method = ( + logger.info if 199 < response_status_code < 300 else logger.warning + ) + logger_method(log_message, extra=data) + + except Exception as exc: + logger.exception(exc) + + return response + + def _abridge(self, data: Any) -> Any: + """ + Abridge data based on length settings. + + Example: + Input: {"name": "Very long name..."} + Output: {"name": "Very long...SHORTENED"} + """ + max_str_len = settings.LOG_MAX_STR_LEN + max_list_len = settings.LOG_MAX_LIST_LEN + + if isinstance(data, dict): + data = {k: self._abridge(v) for k, v in data.items() if k != "meta"} + elif isinstance(data, str) and max_str_len and len(data) > max_str_len: + return f"{data[:max_str_len]}..SHORTENED" + elif isinstance(data, list) and max_list_len: + return [self._abridge(item) for item in data[:max_list_len]] + return data + + @staticmethod + def _empty_value_none( + obj: dict | OrderedDict | str | None, + ) -> dict | OrderedDict | str | None: + """ + Returns None if the value is empty. + + Example: + Input: "" + Output: None + """ + return obj if bool(obj) else None + + @staticmethod + def _mask_sensitive_data( + obj: Any, + ) -> str | dict | None: + """Mask sensitive data in a dictionary based on specified keys and masking style. + + Args: + obj: Dictionary containing potential sensitive data. + + Returns: + dict: A new dictionary with sensitive data masked. + + Example: + Given: + obj = {"password": "my_secret_pass"} + + Returns based on style: + - complete: {"password": "*********MASKED*********"} + - partial: {"password": "my_s*****MASKED*****pass"} + - custom: {"password": "my...ss"} + """ + if not isinstance(obj, dict): + return obj + + data = deepcopy(obj) + data_keys = list(map(str, data.keys())) + + sensitive_keys = settings.LOG_SENSITIVE_KEYS + mask_style = settings.LOG_MASK_STYLE + + def get_mask_function(style): + if style == "complete": + return lambda value: "*********MASKED*********" + elif style == "partial": + return lambda value: f"{value[:4]}*****MASKED*****{value[-4:]}" + elif style == "custom": + custom_style = settings.LOG_MASK_CUSTOM_STYLE + return lambda value: custom_style.format(data=value) + else: + return lambda value: value + + mask_func = get_mask_function(mask_style) + + def _mask(_sensitive_keys: list[str]): + for sensitive_key in _sensitive_keys: + r = re.compile(sensitive_key, flags=re.IGNORECASE) + match_keys = filter(r.match, data_keys) + for key in match_keys: + data[key] = mask_func(data[key]) + + _mask(sensitive_keys) + + return data + + @staticmethod + def _exclude_keys( + obj: dict | OrderedDict | None, keys_to_exclude: list[str] + ) -> dict | None: + """ + Exclude specific keys from a dictionary. + + Example: + Input: {"key1": "value1", "key2": "value2"}, ["key2"] + Output: {"key1": "value1"} + """ + if obj is None: + return None + keys_to_exclude_set = set(map(str.lower, keys_to_exclude)) + return {k: v for k, v in obj.items() if k.lower() not in keys_to_exclude_set} + + def _get_request_data(self, request) -> dict[str, Any]: + """ + Extract necessary request data for logging. + + Example: + Input: Django request object with GET method + Output: {"request": {"method": "GET", ...}} + """ + return { + "request": { + "body": self._get_request_body(request), + "query_params": self._empty_value_none(getattr(request, "GET", None)), + "content_type": self._empty_value_none( + getattr(request, "content_type", None) + ), + "method": self._empty_value_none(getattr(request, "method", None)), + "path": self._empty_value_none(getattr(request, "path", None)), + "headers": self._empty_value_none( + self._exclude_keys( + getattr(request, "headers", None), settings.LOG_EXCLUDED_HEADERS + ) + ), + } + } + + def _get_request_body(self, request) -> str | dict | None: + """ + Extract request body and mask sensitive data. + + Example: + Input: Django request object with JSON body {"key": "value" } + Output: {"key": "value"} + """ + content_type = getattr(request, "content_type", None) + + def decode_and_abridge(body_bytes): + body_str = body_bytes.decode("UTF-8") if body_bytes else None + try: + return self._abridge(json.loads(body_str)) + except Exception: # noqa + return self._abridge(body_str) + + match content_type: + case "multipart/form-data": + return "The image was uploaded to the server" + case "application/json": + return self._mask_sensitive_data( + decode_and_abridge(getattr(request, "body", None)) + ) + case "text/plain": + return self._mask_sensitive_data( + self._abridge(getattr(request, "body", None)) + ) + case _: + return self._mask_sensitive_data(content_type) + + def _get_user_id(self, request) -> Any: + """ + Extract user ID from the request. + + Example: + Input: Django request object with user ID 123 + Output: 123 + """ + return self._empty_value_none( + getattr(request.user, settings.LOG_USER_ID_FIELD, None) + ) + + def _get_user_email(self, request) -> Any: + """ + Extract user email from the request. + + Example: + Input: Django request object with user email "test@example.com" + Output: "test@example.com" + """ + return self._empty_value_none( + getattr(request.user, settings.LOG_USER_EMAIL_FIELD, None) + ) + + @staticmethod + def _is_ignored(request) -> bool: + """ + Determine if the request should be ignored based on path. + + Example: + Input: Django request object with the path "__ignore_me__" + Output: True + """ + default_ignored = request.path.startswith("__") + user_ignored = request.path in settings.LOG_EXCLUDED_ENDPOINTS + return default_ignored or user_ignored diff --git a/django_google_structured_logger/settings.py b/django_google_structured_logger/settings.py new file mode 100644 index 0000000..e6a99ff --- /dev/null +++ b/django_google_structured_logger/settings.py @@ -0,0 +1,36 @@ +from django.conf import settings # type: ignore + +DEFAULT_LOG_SENSITIVE_KEYS = [ + "^password$", + ".*secret.*", + ".*token.*", + ".*key.*", + ".*pass.*", + ".*auth.*", + "^Bearer.*", + ".*ssn.*", # Social Security Number (or equivalent in some countries) + ".*credit.*card.*", # Credit card numbers + ".*cvv.*", # CVV code for credit cards + ".*dob.*", # Date of Birth + ".*pin.*", # Personal Identification Numbers + ".*salt.*", # Salts used in cryptography + ".*encrypt.*", # Encryption keys or related values + ".*api.*", # API keys + ".*jwt.*", # JSON Web Tokens + ".*session.*id.*", # Session Identifiers + "^Authorization$", # Authorization headers + ".*user.*name.*", # Usernames (can sometimes be used in combination with other data for malicious purposes) + ".*address.*", # Physical or email addresses + ".*phone.*", # Phone numbers + "^otp.*", # One-Time Passwords or related values +] +LOG_MAX_STR_LEN = getattr(settings, "LOG_MAX_STR_LEN", 200) +LOG_MAX_LIST_LEN = getattr(settings, "LOG_MAX_LIST_LEN", 10) +LOG_EXCLUDED_ENDPOINTS = getattr(settings, "LOG_EXCLUDED_ENDPOINTS", []) +LOG_SENSITIVE_KEYS = getattr(settings, "LOG_SENSITIVE_KEYS", DEFAULT_LOG_SENSITIVE_KEYS) +LOG_MASK_STYLE = getattr(settings, "LOG_MASK_STYLE", "partially") +LOG_MASK_CUSTOM_STYLE = getattr(settings, "LOG_MASK_CUSTOM_STYLE", "{data}") +LOG_MIDDLEWARE_ENABLED = getattr(settings, "LOG_MIDDLEWARE_ENABLED", True) +LOG_EXCLUDED_HEADERS = getattr(settings, "LOG_EXCLUDED_HEADERS", ["Authorization"]) +LOG_USER_ID_FIELD = getattr(settings, "LOG_USER_ID_FIELD", "id") +LOG_USER_EMAIL_FIELD = getattr(settings, "LOG_USER_EMAIL_FIELD", "email") diff --git a/django_google_structured_logger/storages.py b/django_google_structured_logger/storages.py new file mode 100644 index 0000000..2c43a8e --- /dev/null +++ b/django_google_structured_logger/storages.py @@ -0,0 +1,19 @@ +from contextvars import ContextVar +from dataclasses import dataclass +from typing import Callable + + +@dataclass(frozen=True) +class RequestStorage: + user_id: Callable[[], int] + user_email: Callable[[], str] + uuid: str + + +_current_request: ContextVar[RequestStorage | None] = ContextVar( + "_current_request", default=None +) + + +def get_current_request() -> RequestStorage | None: + return _current_request.get() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1a11a53 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,462 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +category = "dev" +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 = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +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 = "contextvars" +version = "2.4" +description = "PEP 567 Backport" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, +] + +[package.dependencies] +immutables = ">=0.9" + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "django" +version = "4.2.5" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.5-py3-none-any.whl", hash = "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4"}, + {file = "Django-4.2.5.tar.gz", hash = "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "identify" +version = "2.5.28" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.28-py2.py3-none-any.whl", hash = "sha256:87816de144bf46d161bd5b3e8f5596b16cade3b80be537087334b26bc5c177f3"}, + {file = "identify-2.5.28.tar.gz", hash = "sha256:94bb59643083ebd60dc996d043497479ee554381fbc5307763915cda49b0e78f"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "immutables" +version = "0.20" +description = "Immutable Collections" +category = "main" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "immutables-0.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dea0ae4d7f31b145c18c16badeebc2f039d09411be4a8febb86e1244cf7f1ce0"}, + {file = "immutables-0.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dd0dcef2f8d4523d34dbe1d2b7804b3d2a51fddbd104aad13f506a838a2ea15"}, + {file = "immutables-0.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393dde58ffd6b4c089ffdf4cef5fe73dad37ce4681acffade5f5d5935ec23c93"}, + {file = "immutables-0.20-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1214b5a175df783662b7de94b4a82db55cc0ee206dd072fa9e279fb8895d8df"}, + {file = "immutables-0.20-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2761e3dc2a6406943ce77b3505e9b3c1187846de65d7247548dc7edaa202fcba"}, + {file = "immutables-0.20-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bcea81e7516bd823b4ed16f4f794531097888675be13e833b1cc946370d5237"}, + {file = "immutables-0.20-cp310-cp310-win32.whl", hash = "sha256:d828e7580f1fa203ddeab0b5e91f44bf95706e7f283ca9fbbcf0ae08f63d3084"}, + {file = "immutables-0.20-cp310-cp310-win_amd64.whl", hash = "sha256:380e2957ba3d63422b2f3fbbff0547c7bbe6479d611d3635c6411005a4264525"}, + {file = "immutables-0.20-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532be32c7a25dae6cade28825c76d3004cf4d166a0bfacf04bda16056d59ba26"}, + {file = "immutables-0.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5302ce9c7827f8300f3dc34a695abb71e4a32bab09e65e5ad6e454785383347f"}, + {file = "immutables-0.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b51aec54b571ae466113509d4dc79a2808dc2ae9263b71fd6b37778cb49eb292"}, + {file = "immutables-0.20-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f56aea56e597ecf6631f24a4e26007b6a5f4fe30278b96eb90bc1f60506164"}, + {file = "immutables-0.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:085ac48ee3eef7baf070f181cae574489bbf65930a83ec5bbd65c9940d625db3"}, + {file = "immutables-0.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f063f53b5c0e8f541ae381f1d828f3d05bbed766a2d6c817f9218b8b37a4cb66"}, + {file = "immutables-0.20-cp311-cp311-win32.whl", hash = "sha256:b0436cc831b47e26bef637bcf143cf0273e49946cfb7c28c44486d70513a3080"}, + {file = "immutables-0.20-cp311-cp311-win_amd64.whl", hash = "sha256:5bb32aee1ea16fbb90f58f8bd96016bca87aba0a8e574e5fa218d0d83b142851"}, + {file = "immutables-0.20-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ba726b7a3a696b9d4b122fa2c956bc68e866f3df1b92765060c88c64410ff82"}, + {file = "immutables-0.20-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5a88adf1dcc9d8ab07dba5e74deefcd5b5e38bc677815cbf9365dc43b69f1f08"}, + {file = "immutables-0.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1009a4e00e2e69a9b40c2f1272795f5a06ad72c9bf4638594d518e9cbd7a721a"}, + {file = "immutables-0.20-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96899994842c37cf4b9d6d2bedf685aae7810bd73f1538f8cba5426e2d65cb85"}, + {file = "immutables-0.20-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a606410b2ccb6ae339c3f26cccc9a92bcb16dc06f935d51edfd8ca68cf687e50"}, + {file = "immutables-0.20-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8e82754f72823085643a2c0e6a4c489b806613e94af205825fa81df2ba147a0"}, + {file = "immutables-0.20-cp312-cp312-win32.whl", hash = "sha256:525fb361bd7edc8a891633928d549713af8090c79c25af5cc06eb90b48cb3c64"}, + {file = "immutables-0.20-cp312-cp312-win_amd64.whl", hash = "sha256:a82afc3945e9ceb9bcd416dc4ed9b72f92760c42787e26de50610a8b81d48120"}, + {file = "immutables-0.20-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f17f25f21e82a1c349a61191cfb13e442a348b880b74cb01b00e0d1e848b63f4"}, + {file = "immutables-0.20-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:65954eb861c61af48debb1507518d45ae7d594b4fba7282785a70b48c5f51f9b"}, + {file = "immutables-0.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62f8a7a22939278127b7a206d05679b268b9cf665437125625348e902617cbad"}, + {file = "immutables-0.20-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac86f4372f4cfaa00206c12472fd3a78753092279e0552b7e1880944d71b04fe"}, + {file = "immutables-0.20-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e771198edc11a9e02ffa693911b3918c6cde0b64ad2e6672b076dbe005557ad8"}, + {file = "immutables-0.20-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc739fc07cff5df2e4f31addbd48660b5ac0da56e9f719f8bb45da8ddd632c63"}, + {file = "immutables-0.20-cp38-cp38-win32.whl", hash = "sha256:c086ccb44d9d3824b9bf816365d10b1b82837efc7119f8bab56bd7a27ed805a9"}, + {file = "immutables-0.20-cp38-cp38-win_amd64.whl", hash = "sha256:9cd2ee9c10bf00be3c94eb51854bc0b761326bd0a7ea0dad4272a3f182269ae6"}, + {file = "immutables-0.20-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4f78cb748261f852953620ed991de74972446fd484ec69377a41e2f1a1beb75"}, + {file = "immutables-0.20-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6449186ea91b7c17ec8e7bd9bf059858298b1db5c053f5d27de8eba077578ce"}, + {file = "immutables-0.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85dd9765b068f7beb297553fddfcf7f904bd58a184c520830a106a58f0c9bfb4"}, + {file = "immutables-0.20-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f349a7e0327b92dcefb863e49ace086f2f26e6689a4e022c98720c6e9696e763"}, + {file = "immutables-0.20-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e3a5462f6d3549bbf7d02ce929fb0cb6df9539445f0589105de4e8b99b906e69"}, + {file = "immutables-0.20-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc51a01a64a6d2cd7db210a49ad010c2ac2e9e026745f23fd31e0784096dcfff"}, + {file = "immutables-0.20-cp39-cp39-win32.whl", hash = "sha256:83794712f0507416f2818edc63f84305358b8656a93e5b9e2ab056d9803c7507"}, + {file = "immutables-0.20-cp39-cp39-win_amd64.whl", hash = "sha256:2837b1078abc66d9f009bee9085cf62515d5516af9a5c9ea2751847e16efd236"}, + {file = "immutables-0.20.tar.gz", hash = "sha256:1d2f83e6a6a8455466cd97b9a90e2b4f7864648616dfa6b19d18f49badac3876"}, +] + +[package.extras] +test = ["flake8 (>=5.0,<6.0)", "mypy (>=1.4,<2.0)", "pycodestyle (>=2.9,<3.0)", "pytest (>=7.4,<8.0)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +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 = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.4.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, + {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +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.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "virtualenv" +version = "20.24.5" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "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)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "d47f0389c319d69bf3069af493fa7ccbff98d76b12537fba87374723f531017f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7f99dfd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "django-google-structured-logger" +version = "0.1.0" +description = "Django library for Google Structured Logger (GSL)" +authors = ["Farruh Sheripov "] +readme = "README.md" +packages = [{include = "django_google_structured_logger"}] + +[tool.poetry.dependencies] +python = "^3.10" +Django = "^4.2.5" +python-json-logger = "^2.0.7" +contextvars = "^2.4" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.2" +pre-commit = "^3.4.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..beb4dce --- /dev/null +++ b/setup.cfg @@ -0,0 +1,44 @@ +[metadata] +name = django-google-structured-logger +url = https://github.com/muehlemann-popp/django-google-structured-logger +description = Plugin for django to support Google Structured Logger +long_description = file: README.md +long_description_content_type = text/markdown +description_file = README.md +author = Farruh Sheripiov +author_email = farruh.sheripov@muehlemann-popp.ch +maintainer = Silvan Mühlemann +maintainer_email = silvan.muehlemann@muehlemann-popp.ch +license='MIT' +classifiers = + Intended Audience :: Developers + Development Status :: 5 - Production/Stable + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Topic :: Software Development + Framework :: Django + Framework :: Django :: 4.0 + Framework :: Django :: 4.1 + Programming Language :: Python + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + +[options] +include_package_data = true +packages = find: +python_requires = >=3.8 +install_requires = + Django >= 4.0 + +[upload] +repository = mpom +show_response = 1 + +[flake8] +exclude=.tox,.git,*/migrations/*,*/static/*,docs,.venv,__init__.py +ignore = E203, E266, E501, W503 +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4d78a94 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup # type: ignore + +setup( + use_scm_version=True, + setup_requires=["setuptools_scm"], +)