Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.11]
django-version: [Django~=4.2]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
django-version: ["django~=4.2", "django~=5.1", "django~=5.2"]

steps:
- uses: actions/checkout@v2
Expand All @@ -24,8 +24,8 @@ jobs:
- name: Install Dependencies
run: |
pip install ${{ matrix.django-version }}
python setup.py install
pip install -r test_requirements.txt
pip install .
- name: Run Tests
run: |
python runtests.py
24 changes: 18 additions & 6 deletions gdpr/anonymizers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,14 @@ def __init__(self, max_anonymization_range: int = None, ignore_empty_values: boo
self.max_anonymization_range = max_anonymization_range
super().__init__(ignore_empty_values, empty_values)

def get_numeric_encryption_key(self, encryption_key: str, value: Union[int, float] = None) -> int:
def get_numeric_encryption_key(
self,
encryption_key: str,
value: Union[int, float] = None,
allow_zero: bool = True
) -> int:
"""
From `encryption_key` create it's numeric counterpart of appropriate length.
From `encryption_key` create its numeric counterpart of appropriate length.

If value is supplied then the appropriate length is based on it if not the
parameter `self.max_anonymization_range` is used.
Expand All @@ -151,11 +156,18 @@ def get_numeric_encryption_key(self, encryption_key: str, value: Union[int, floa

:param encryption_key: The encryption key generated by anonymizer.
:param value: Value to which the result of this function will be used.
:param allow_zero: VWhether returning zero is permitted.
:return: Numeric counterpart of encryption_key
"""
if value is None:
if self.max_anonymization_range is None:
return numerize_key(encryption_key)
return numerize_key(encryption_key) % self.max_anonymization_range

return numerize_key(encryption_key) % 10 ** get_number_guess_len(value)
numeric_key = numerize_key(encryption_key)
else:
numeric_key = numerize_key(encryption_key) % self.max_anonymization_range
else:
numeric_key = numerize_key(encryption_key) % (10 ** get_number_guess_len(value))

if numeric_key == 0 and not allow_zero:
return 1
else:
return numeric_key
19 changes: 14 additions & 5 deletions gdpr/anonymizers/gis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Since django 1.11 djnago-GIS requires GDAL."""

import logging
from typing import Optional

Expand Down Expand Up @@ -50,18 +51,26 @@ def get_encrypted_value(self, value, encryption_key: str):
from django.contrib.gis.geos import Point

new_val: Point = Point(value.tuple)
new_val.x = (new_val.x + self.get_numeric_encryption_key(encryption_key, int(new_val.x))) % self.max_x_range
new_val.y = (new_val.y + self.get_numeric_encryption_key(encryption_key, int(new_val.y))) % self.max_y_range
new_val.x = (
new_val.x + self.get_numeric_encryption_key(encryption_key, int(new_val.x), allow_zero=False)
) % self.max_x_range
new_val.y = (
new_val.y + self.get_numeric_encryption_key(encryption_key, int(new_val.y), allow_zero=False)
) % self.max_y_range

return new_val

def get_decrypted_value(self, value, encryption_key: str):
if not is_gis_installed():
raise ImproperlyConfigured('Unable to load django GIS.')
raise ImproperlyConfigured("Unable to load django GIS.")
from django.contrib.gis.geos import Point

new_val: Point = Point(value.tuple)
new_val.x = (new_val.x - self.get_numeric_encryption_key(encryption_key, int(new_val.x))) % self.max_x_range
new_val.y = (new_val.y - self.get_numeric_encryption_key(encryption_key, int(new_val.y))) % self.max_y_range
new_val.x = (
new_val.x - self.get_numeric_encryption_key(encryption_key, int(new_val.x), allow_zero=False)
) % self.max_x_range
new_val.y = (
new_val.y - self.get_numeric_encryption_key(encryption_key, int(new_val.y), allow_zero=False)
) % self.max_y_range

return new_val
8 changes: 4 additions & 4 deletions gdpr/anonymizers/local/cs.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def get_encrypted_value(self, value, encryption_key: str):
account = CzechAccountNumber.parse(value)

if self.use_smart_method and account.check_account_format():
return str(account.brute_force_next(self.get_numeric_encryption_key(encryption_key)))
return str(account.brute_force_next(self.get_numeric_encryption_key(encryption_key, allow_zero=False)))

account.num = int(encrypt_text(encryption_key, str(account.num), NUMBERS))

Expand All @@ -355,7 +355,7 @@ def get_decrypted_value(self, value: Any, encryption_key: str):
account = CzechAccountNumber.parse(value)

if self.use_smart_method and account.check_account_format():
return str(account.brute_force_prev(self.get_numeric_encryption_key(encryption_key)))
return str(account.brute_force_prev(self.get_numeric_encryption_key(encryption_key, allow_zero=False)))

account.num = int(decrypt_text(encryption_key, str(account.num), NUMBERS))

Expand All @@ -369,13 +369,13 @@ def get_encrypted_value(self, value: Any, encryption_key: str):
iban = CzechIBAN.parse(value)
if not iban.check_iban_format():
raise ValidationError(f'IBAN \'{value}\' does not appear to be valid czech IBAN.')
return str(iban.brute_force_next(self.get_numeric_encryption_key(encryption_key)))
return str(iban.brute_force_next(self.get_numeric_encryption_key(encryption_key, allow_zero=False)))

def get_decrypted_value(self, value: Any, encryption_key: str):
iban = CzechIBAN.parse(value)
if not iban.check_iban_format():
raise ValidationError(f'IBAN \'{value}\' does not appear to be valid czech IBAN.')
return str(iban.brute_force_prev(self.get_numeric_encryption_key(encryption_key)))
return str(iban.brute_force_prev(self.get_numeric_encryption_key(encryption_key, allow_zero=False)))


class CzechPhoneNumberFieldAnonymizer(FieldAnonymizer):
Expand Down
7 changes: 4 additions & 3 deletions gdpr/migrations/0002_auto_20180509_1518.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import datetime

from django.db import migrations, models
from django.utils.timezone import utc


class Migration(migrations.Migration):
Expand All @@ -22,8 +21,10 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='legalreason',
name='issued_at',
field=models.DateTimeField(default=datetime.datetime(2018, 5, 9, 13, 18, 7, 317147, tzinfo=utc),
verbose_name='issued at'),
field=models.DateTimeField(
default=datetime.datetime(2018, 5, 9, 13, 18, 7, 317147, tzinfo=datetime.timezone.utc),
verbose_name='issued at'
),
preserve_default=False,
),
migrations.AlterField(
Expand Down
20 changes: 20 additions & 0 deletions gdpr/tests/test_cs_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TestCzechAccountNumberField(TestCase):
def setUpTestData(cls):
cls.field = CzechAccountNumberFieldAnonymizer()
cls.encryption_key = 'LoremIpsumDolorSitAmet'
cls.zero_remainder_encryption_key = 'Vu'

def test_account_number_simple_field(self):
account_number = '2501277007/2010'
Expand All @@ -35,6 +36,17 @@ def test_account_number_simple_field_smart_method(self):

assert_equal(out_decrypt, account_number)

def test_account_number_simple_field_smart_method_using_encryption_key_having_zero_modulo_remainder(self):
field = CzechAccountNumberFieldAnonymizer(use_smart_method=True)
account_number = '2501277007/2010'
out = field.get_encrypted_value(account_number, self.zero_remainder_encryption_key)

assert_not_equal(out, account_number)

out_decrypt = field.get_decrypted_value(out, self.zero_remainder_encryption_key)

assert_equal(out_decrypt, account_number)

def test_account_number_with_pre_num_field(self):
account_number = '19-2000145399/0800'
out = self.field.get_encrypted_value(account_number, self.encryption_key)
Expand Down Expand Up @@ -79,6 +91,7 @@ class TestCzechIBANSmartFieldAnonymizer(TestCase):
def setUpTestData(cls):
cls.field = CzechIBANSmartFieldAnonymizer()
cls.encryption_key = 'LoremIpsumDolorSitAmet'
cls.zero_remainder_encryption_key = 'Vu'
cls.text_iban = 'CZ65 0800 0000 1920 0014 5399'
cls.no_space_text_iban = 'CZ6508000000192000145399'
cls.invalid_text_iban = 'CZ00 0800 0000 1920 0014 5399'
Expand All @@ -91,6 +104,13 @@ def test_czech_iban_field(self):
out_decrypted = self.field.get_decrypted_value(out, self.encryption_key)
assert_equal(out_decrypted, self.text_iban)

def test_czech_iban_field_using_encryption_key_having_zero_modulo_remainder(self):
out = self.field.get_encrypted_value(self.text_iban, self.zero_remainder_encryption_key)
assert_not_equal(out, self.text_iban)

out_decrypted = self.field.get_decrypted_value(out, self.zero_remainder_encryption_key)
assert_equal(out_decrypted, self.text_iban)

def test_czech_iban_field_no_space(self):
out = self.field.get_encrypted_value(self.no_space_text_iban, self.encryption_key)
assert_not_equal(out, self.no_space_text_iban)
Expand Down
18 changes: 10 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ def read(fname):
classifiers=[
'Development Status :: 3 - Alpha',
'Framework :: Django',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.1',
'Framework :: Django :: 5.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: 3 :: Only',
'Intended Audience :: Developers',
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a duplicate definition.

'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
python_requires='>=3.10',
Copy link
Copy Markdown
Author

@sp-tm sp-tm Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm setting 3.10 as the minimum version because:

  1. Previous versions have already reached EOL.
  2. The code was already incompatible with versions < 3.10, e.g. because of declarations like Type[Model] | None while not importing from __future__.

install_requires=[
'django>=4.2',
'django>=4.2,<6',
'skip-django-chamber>=0.7.2',
'tqdm>=4.28.1',
'pyaes>=1.6.1',
Expand Down
5 changes: 3 additions & 2 deletions test_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ flake8==3.8.4
mypy==0.991
python-dateutil==2.7.5
django-extensions
freezegun==0.3.12
freezegun
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old version doesn't work in Python >= 3.13.

Faker==1.0.1
skip-django-germanium==2.4.0
skip-django-germanium==2.6.0
django-auditlog
setuptools
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed for Python >= 3.12.