django-gc 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. django_gc-1.0.1/LICENSE +21 -0
  2. django_gc-1.0.1/PKG-INFO +136 -0
  3. django_gc-1.0.1/README.md +100 -0
  4. django_gc-1.0.1/django_gc/__init__.py +45 -0
  5. django_gc-1.0.1/django_gc/admins/__init__.py +4 -0
  6. django_gc-1.0.1/django_gc/admins/global_config.py +48 -0
  7. django_gc-1.0.1/django_gc/admins/global_config_category.py +45 -0
  8. django_gc-1.0.1/django_gc/apps.py +21 -0
  9. django_gc-1.0.1/django_gc/conf.py +101 -0
  10. django_gc-1.0.1/django_gc/dto/__init__.py +4 -0
  11. django_gc-1.0.1/django_gc/dto/definitions.py +33 -0
  12. django_gc-1.0.1/django_gc/dto/global_config_cache_value.py +16 -0
  13. django_gc-1.0.1/django_gc/enums/__init__.py +4 -0
  14. django_gc-1.0.1/django_gc/enums/choices_enum.py +36 -0
  15. django_gc-1.0.1/django_gc/enums/setting_types.py +68 -0
  16. django_gc-1.0.1/django_gc/exceptions.py +77 -0
  17. django_gc-1.0.1/django_gc/fields.py +35 -0
  18. django_gc-1.0.1/django_gc/forms/__init__.py +4 -0
  19. django_gc-1.0.1/django_gc/forms/global_config_categories.py +21 -0
  20. django_gc-1.0.1/django_gc/forms/global_configs.py +147 -0
  21. django_gc-1.0.1/django_gc/migrations/0001_initial.py +54 -0
  22. django_gc-1.0.1/django_gc/migrations/__init__.py +0 -0
  23. django_gc-1.0.1/django_gc/models/__init__.py +4 -0
  24. django_gc-1.0.1/django_gc/models/global_config.py +68 -0
  25. django_gc-1.0.1/django_gc/models/global_config_category.py +24 -0
  26. django_gc-1.0.1/django_gc/py.typed +0 -0
  27. django_gc-1.0.1/django_gc/services/__init__.py +43 -0
  28. django_gc-1.0.1/django_gc/services/cache.py +97 -0
  29. django_gc-1.0.1/django_gc/services/global_config_initialization_service.py +146 -0
  30. django_gc-1.0.1/django_gc/services/global_config_service.py +269 -0
  31. django_gc-1.0.1/django_gc/signals/__init__.py +11 -0
  32. django_gc-1.0.1/django_gc/signals/callbacks.py +15 -0
  33. django_gc-1.0.1/django_gc/signals/initialize_global_configs.py +17 -0
  34. django_gc-1.0.1/django_gc/signals/normalize_global_config_value.py +30 -0
  35. django_gc-1.0.1/django_gc/signals/prevent_global_config_deletion.py +15 -0
  36. django_gc-1.0.1/django_gc/signals/refresh_global_configs_cache.py +19 -0
  37. django_gc-1.0.1/django_gc/singleton.py +17 -0
  38. django_gc-1.0.1/django_gc/tasks/__init__.py +3 -0
  39. django_gc-1.0.1/django_gc/tasks/refresh_global_configs_cache.py +19 -0
  40. django_gc-1.0.1/django_gc/utils.py +15 -0
  41. django_gc-1.0.1/django_gc.egg-info/PKG-INFO +136 -0
  42. django_gc-1.0.1/django_gc.egg-info/SOURCES.txt +54 -0
  43. django_gc-1.0.1/django_gc.egg-info/dependency_links.txt +1 -0
  44. django_gc-1.0.1/django_gc.egg-info/requires.txt +5 -0
  45. django_gc-1.0.1/django_gc.egg-info/top_level.txt +1 -0
  46. django_gc-1.0.1/pyproject.toml +87 -0
  47. django_gc-1.0.1/setup.cfg +4 -0
  48. django_gc-1.0.1/tests/test_admin.py +77 -0
  49. django_gc-1.0.1/tests/test_encryption.py +32 -0
  50. django_gc-1.0.1/tests/test_global_config_service.py +271 -0
  51. django_gc-1.0.1/tests/test_global_config_signals.py +66 -0
  52. django_gc-1.0.1/tests/test_initialization.py +137 -0
  53. django_gc-1.0.1/tests/test_json_parse.py +34 -0
  54. django_gc-1.0.1/tests/test_models.py +62 -0
  55. django_gc-1.0.1/tests/test_task.py +28 -0
  56. django_gc-1.0.1/tests/test_types.py +86 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RDD Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-gc
3
+ Version: 1.0.1
4
+ Summary: Typed Django runtime configuration stored in the database and served from cache
5
+ Author-email: Raman Marozau <r.morozov@rdd-lab.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://django-gc.rdd-lab.com/
8
+ Project-URL: Documentation, https://django-gc.rdd-lab.com/
9
+ Project-URL: Issues, https://github.com/RDDLab/Django-GC/issues
10
+ Project-URL: Repository, https://github.com/RDDLab/Django-GC
11
+ Project-URL: Changelog, https://github.com/RDDLab/Django-GC/blob/main/CHANGELOG.md
12
+ Keywords: django,config,settings,cache,admin
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: Django :: 6.0
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.12
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: django>=6.0
32
+ Requires-Dist: cryptography>=44.0.0
33
+ Provides-Extra: celery
34
+ Requires-Dist: celery>=5.3; extra == "celery"
35
+ Dynamic: license-file
36
+
37
+ ![Logo](https://github.com/RDDLab/Django-GC/raw/main/docs/static/img/logo.svg)
38
+
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=for-the-badge)](https://docs.astral.sh/ruff)
41
+ [![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json&style=for-the-badge)](https://pyrefly.org)
42
+ [![PyPI](https://img.shields.io/pypi/v/django-gc?style=for-the-badge)](https://pypi.org/project/django-gc/)
43
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc/)
44
+ [![PyPI djversions](https://img.shields.io/pypi/djversions/django-gc.svg?style=for-the-badge)](https://pypi.org/project/django-gc/)
45
+ [![PyPI status](https://img.shields.io/pypi/status/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc)
46
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-gc?style=for-the-badge)](https://pypistats.org/packages/django-gc)
47
+ [![PyPI - Types](https://img.shields.io/pypi/types/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc)
48
+
49
+ ---
50
+
51
+ [![Tests](https://img.shields.io/github/actions/workflow/status/RDDLab/Django-GC/testing.yml?branch=main&label=Tests)](https://github.com/RDDLab/Django-GC/actions/workflows/testing.yml)
52
+ [![Codecov](https://img.shields.io/codecov/c/github/RDDLab/Django-GC/main?logo=codecov)](https://codecov.io/gh/RDDLab/Django-GC)
53
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/RDDLab/Django-GC/main.svg)](https://results.pre-commit.ci/latest/github/RDDLab/Django-GC/main)
54
+
55
+ ---
56
+
57
+ **Documentation**: <a href="https://django-gc.rdd-lab.com/" target="_blank">https://django-gc.rdd-lab.com/</a>
58
+
59
+ **Source Code**: <a href="https://github.com/RDDLab/Django-GC" target="_blank">https://github.com/RDDLab/Django-GC</a>
60
+
61
+ ---
62
+
63
+ # Django-GC
64
+
65
+ Django-GC stores typed runtime settings in the database and serves them from Django's cache. Application code never reads a value through the ORM. The project owns the keys; this package is the mechanism.
66
+
67
+ ## Features
68
+
69
+ - **Code-declared keys** — categories and definitions live in Django settings and are synchronized after migrate.
70
+ - **Typed values** — strings, numbers, dates, arrays, choices, JSON, UUID, and optional secure secrets.
71
+ - **Full cache snapshot** — `get_value()` never falls back to a single-row ORM read.
72
+ - **Stock Django Admin** — edit values with standard `ModelAdmin` and Django's built-in change history.
73
+ - **Optional Celery** — periodic full snapshot refresh.
74
+ - **Fully typed** — ships `py.typed`; compatible with pyrefly and standard type checkers.
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install django-gc
80
+ ```
81
+
82
+ ```python
83
+ INSTALLED_APPS = [
84
+ 'django_gc',
85
+ 'django.contrib.admin',
86
+ # ...
87
+ ]
88
+ ```
89
+
90
+ Periodic refresh is optional:
91
+
92
+ ```bash
93
+ pip install 'django-gc[celery]'
94
+ ```
95
+
96
+ ## Quick start
97
+
98
+ In Django `settings`, set categories, keys, and the encryption key. Then migrate and read values through the service:
99
+
100
+ ```python
101
+ from enum import StrEnum
102
+
103
+ from django_gc import (
104
+ CategoryDefinition,
105
+ SettingDefinition,
106
+ SettingType,
107
+ get_value,
108
+ set_value,
109
+ )
110
+
111
+
112
+ class SettingKey(StrEnum):
113
+ MAINTENANCE_ENABLED = 'maintenance-enabled'
114
+
115
+
116
+ GLOBAL_CONFIG_CATEGORIES = [
117
+ CategoryDefinition(id=1, code='platform', name='Platform'),
118
+ ]
119
+ GLOBAL_CONFIG_DEFINITIONS = [
120
+ SettingDefinition(
121
+ key=SettingKey.MAINTENANCE_ENABLED,
122
+ description='Maintenance mode',
123
+ default_value=False,
124
+ value_type=SettingType.BOOLEAN,
125
+ category_id=1,
126
+ ),
127
+ ]
128
+ GLOBAL_CONFIG_ENCRYPTION_KEY = 'replace-with-a-long-random-secret'
129
+
130
+ enabled = get_value(SettingKey.MAINTENANCE_ENABLED)
131
+ set_value(SettingKey.MAINTENANCE_ENABLED, True)
132
+ ```
133
+
134
+ ## Documentation
135
+
136
+ **https://django-gc.rdd-lab.com/**
@@ -0,0 +1,100 @@
1
+ ![Logo](https://github.com/RDDLab/Django-GC/raw/main/docs/static/img/logo.svg)
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
4
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=for-the-badge)](https://docs.astral.sh/ruff)
5
+ [![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json&style=for-the-badge)](https://pyrefly.org)
6
+ [![PyPI](https://img.shields.io/pypi/v/django-gc?style=for-the-badge)](https://pypi.org/project/django-gc/)
7
+ [![PyPI pyversions](https://img.shields.io/pypi/pyversions/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc/)
8
+ [![PyPI djversions](https://img.shields.io/pypi/djversions/django-gc.svg?style=for-the-badge)](https://pypi.org/project/django-gc/)
9
+ [![PyPI status](https://img.shields.io/pypi/status/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc)
10
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-gc?style=for-the-badge)](https://pypistats.org/packages/django-gc)
11
+ [![PyPI - Types](https://img.shields.io/pypi/types/django-gc.svg?style=for-the-badge)](https://pypi.python.org/pypi/django-gc)
12
+
13
+ ---
14
+
15
+ [![Tests](https://img.shields.io/github/actions/workflow/status/RDDLab/Django-GC/testing.yml?branch=main&label=Tests)](https://github.com/RDDLab/Django-GC/actions/workflows/testing.yml)
16
+ [![Codecov](https://img.shields.io/codecov/c/github/RDDLab/Django-GC/main?logo=codecov)](https://codecov.io/gh/RDDLab/Django-GC)
17
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/RDDLab/Django-GC/main.svg)](https://results.pre-commit.ci/latest/github/RDDLab/Django-GC/main)
18
+
19
+ ---
20
+
21
+ **Documentation**: <a href="https://django-gc.rdd-lab.com/" target="_blank">https://django-gc.rdd-lab.com/</a>
22
+
23
+ **Source Code**: <a href="https://github.com/RDDLab/Django-GC" target="_blank">https://github.com/RDDLab/Django-GC</a>
24
+
25
+ ---
26
+
27
+ # Django-GC
28
+
29
+ Django-GC stores typed runtime settings in the database and serves them from Django's cache. Application code never reads a value through the ORM. The project owns the keys; this package is the mechanism.
30
+
31
+ ## Features
32
+
33
+ - **Code-declared keys** — categories and definitions live in Django settings and are synchronized after migrate.
34
+ - **Typed values** — strings, numbers, dates, arrays, choices, JSON, UUID, and optional secure secrets.
35
+ - **Full cache snapshot** — `get_value()` never falls back to a single-row ORM read.
36
+ - **Stock Django Admin** — edit values with standard `ModelAdmin` and Django's built-in change history.
37
+ - **Optional Celery** — periodic full snapshot refresh.
38
+ - **Fully typed** — ships `py.typed`; compatible with pyrefly and standard type checkers.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install django-gc
44
+ ```
45
+
46
+ ```python
47
+ INSTALLED_APPS = [
48
+ 'django_gc',
49
+ 'django.contrib.admin',
50
+ # ...
51
+ ]
52
+ ```
53
+
54
+ Periodic refresh is optional:
55
+
56
+ ```bash
57
+ pip install 'django-gc[celery]'
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ In Django `settings`, set categories, keys, and the encryption key. Then migrate and read values through the service:
63
+
64
+ ```python
65
+ from enum import StrEnum
66
+
67
+ from django_gc import (
68
+ CategoryDefinition,
69
+ SettingDefinition,
70
+ SettingType,
71
+ get_value,
72
+ set_value,
73
+ )
74
+
75
+
76
+ class SettingKey(StrEnum):
77
+ MAINTENANCE_ENABLED = 'maintenance-enabled'
78
+
79
+
80
+ GLOBAL_CONFIG_CATEGORIES = [
81
+ CategoryDefinition(id=1, code='platform', name='Platform'),
82
+ ]
83
+ GLOBAL_CONFIG_DEFINITIONS = [
84
+ SettingDefinition(
85
+ key=SettingKey.MAINTENANCE_ENABLED,
86
+ description='Maintenance mode',
87
+ default_value=False,
88
+ value_type=SettingType.BOOLEAN,
89
+ category_id=1,
90
+ ),
91
+ ]
92
+ GLOBAL_CONFIG_ENCRYPTION_KEY = 'replace-with-a-long-random-secret'
93
+
94
+ enabled = get_value(SettingKey.MAINTENANCE_ENABLED)
95
+ set_value(SettingKey.MAINTENANCE_ENABLED, True)
96
+ ```
97
+
98
+ ## Documentation
99
+
100
+ **https://django-gc.rdd-lab.com/**
@@ -0,0 +1,45 @@
1
+ from typing import Any
2
+
3
+ from django_gc.dto import CategoryDefinition, SettingDefinition
4
+ from django_gc.enums import SettingType
5
+ from django_gc.exceptions import (
6
+ GlobalConfigCreationForbiddenError,
7
+ GlobalConfigDeletionForbiddenError,
8
+ GlobalConfigError,
9
+ GlobalConfigLockNotOwnedError,
10
+ GlobalConfigNotFoundError,
11
+ GlobalConfigParseError,
12
+ GlobalConfigRefreshLockTimeoutError,
13
+ GlobalConfigSnapshotIncompleteError,
14
+ GlobalConfigValueError,
15
+ )
16
+
17
+ __all__ = [
18
+ 'CategoryDefinition',
19
+ 'GlobalConfigCreationForbiddenError',
20
+ 'GlobalConfigDeletionForbiddenError',
21
+ 'GlobalConfigError',
22
+ 'GlobalConfigLockNotOwnedError',
23
+ 'GlobalConfigNotFoundError',
24
+ 'GlobalConfigParseError',
25
+ 'GlobalConfigRefreshLockTimeoutError',
26
+ 'GlobalConfigSnapshotIncompleteError',
27
+ 'GlobalConfigValueError',
28
+ 'SettingDefinition',
29
+ 'SettingType',
30
+ 'get_value',
31
+ 'is_cache_ready',
32
+ 'refresh_cache',
33
+ 'set_value',
34
+ ]
35
+
36
+
37
+ def __getattr__(name: str) -> Any:
38
+ """
39
+ Лениво отдать runtime API после загрузки Django apps.
40
+ """
41
+ if name in {'get_value', 'is_cache_ready', 'refresh_cache', 'set_value'}:
42
+ from django_gc import services
43
+
44
+ return getattr(services, name)
45
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
@@ -0,0 +1,4 @@
1
+ from django_gc.admins.global_config import GlobalConfigAdmin
2
+ from django_gc.admins.global_config_category import GlobalConfigCategoryAdmin
3
+
4
+ __all__ = ['GlobalConfigAdmin', 'GlobalConfigCategoryAdmin']
@@ -0,0 +1,48 @@
1
+ from typing import TYPE_CHECKING, override
2
+
3
+ from django.contrib import admin
4
+ from django.http import HttpRequest
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+ from django_gc.forms import GlobalConfigForm
8
+ from django_gc.models import GlobalConfig
9
+
10
+ if TYPE_CHECKING:
11
+ _GlobalConfigAdmin = admin.ModelAdmin[GlobalConfig]
12
+ else:
13
+ _GlobalConfigAdmin = admin.ModelAdmin
14
+
15
+
16
+ @admin.register(GlobalConfig)
17
+ class GlobalConfigAdmin(_GlobalConfigAdmin):
18
+ """
19
+ Управлять значениями GlobalConfig без изменения состава системных ключей.
20
+ """
21
+
22
+ form = GlobalConfigForm
23
+ list_display = ['key', 'description', 'category']
24
+ list_select_related = ['category']
25
+ list_filter = ['category', 'value_type', 'is_read_only']
26
+ search_fields = ['key', 'description']
27
+ ordering = ['category', 'description']
28
+ readonly_fields = ['key', 'description', 'is_read_only', 'category', 'created_at', 'updated_at']
29
+ fieldsets = [
30
+ (_('Общая информация'), {'classes': ['wide'], 'fields': ['key', 'category', 'value_type', 'description']}),
31
+ (_('Параметры'), {'classes': ['wide'], 'fields': ['is_read_only']}),
32
+ (_('Значение'), {'classes': ['wide'], 'fields': ['value']}),
33
+ (_('Даты'), {'classes': ['wide'], 'fields': ['created_at', 'updated_at']}),
34
+ ]
35
+
36
+ @override
37
+ def has_add_permission(self, request: HttpRequest) -> bool:
38
+ """
39
+ Разрешить создание ключей только через definitions.
40
+ """
41
+ return False
42
+
43
+ @override
44
+ def has_delete_permission(self, request: HttpRequest, obj: GlobalConfig | None = None) -> bool:
45
+ """
46
+ Запретить удаление системных ключей.
47
+ """
48
+ return False
@@ -0,0 +1,45 @@
1
+ from typing import TYPE_CHECKING, override
2
+
3
+ from django.contrib import admin
4
+ from django.http import HttpRequest
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+ from django_gc.forms import GlobalConfigCategoryForm
8
+ from django_gc.models import GlobalConfigCategory
9
+
10
+ if TYPE_CHECKING:
11
+ _GlobalConfigCategoryAdmin = admin.ModelAdmin[GlobalConfigCategory]
12
+ else:
13
+ _GlobalConfigCategoryAdmin = admin.ModelAdmin
14
+
15
+
16
+ @admin.register(GlobalConfigCategory)
17
+ class GlobalConfigCategoryAdmin(_GlobalConfigCategoryAdmin):
18
+ """
19
+ Разрешить переименование системных категорий без изменения их состава.
20
+ """
21
+
22
+ form = GlobalConfigCategoryForm
23
+ list_display = ['id', 'code', 'name']
24
+ search_fields = ['code', 'name']
25
+ ordering = ['id']
26
+ readonly_fields = ['id', 'code', 'created_at', 'updated_at']
27
+ fieldsets = [
28
+ (_('Идентификаторы'), {'classes': ['wide'], 'fields': ['id', 'code']}),
29
+ (_('Категория'), {'classes': ['wide'], 'fields': ['name']}),
30
+ (_('Даты'), {'classes': ['wide'], 'fields': ['created_at', 'updated_at']}),
31
+ ]
32
+
33
+ @override
34
+ def has_add_permission(self, request: HttpRequest) -> bool:
35
+ """
36
+ Разрешить создание категорий только через definitions.
37
+ """
38
+ return False
39
+
40
+ @override
41
+ def has_delete_permission(self, request: HttpRequest, obj: GlobalConfigCategory | None = None) -> bool:
42
+ """
43
+ Запретить удаление системных категорий.
44
+ """
45
+ return False
@@ -0,0 +1,21 @@
1
+ from typing import override
2
+
3
+ from django.apps import AppConfig
4
+ from django.utils.translation import gettext_lazy as _
5
+
6
+
7
+ class Config(AppConfig):
8
+ """
9
+ Приложение типизированных runtime-настроек.
10
+ """
11
+
12
+ default_auto_field = 'django.db.models.BigAutoField'
13
+ name = 'django_gc'
14
+ verbose_name = _('Управление настройками')
15
+
16
+ @override
17
+ def ready(self) -> None:
18
+ """
19
+ Подключить сигналы и регистрацию Admin.
20
+ """
21
+ from django_gc import admins, signals
@@ -0,0 +1,101 @@
1
+ import base64
2
+ import hashlib
3
+ from collections.abc import Sequence
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar, Token
6
+ from typing import Any
7
+
8
+ from cryptography.fernet import Fernet, InvalidToken
9
+ from django.conf import settings
10
+
11
+ from django_gc.dto import CategoryDefinition, SettingDefinition
12
+
13
+ _REFRESH_SUPPRESSED: ContextVar[bool] = ContextVar('global_config_refresh_suppressed', default=False)
14
+
15
+
16
+ def get_cache_alias() -> str:
17
+ """
18
+ Вернуть алиас Django cache для снимка GlobalConfig.
19
+ """
20
+ return str(getattr(settings, 'GLOBAL_CONFIG_CACHE_ALIAS', 'default'))
21
+
22
+
23
+ def get_category_definitions() -> Sequence[CategoryDefinition]:
24
+ """
25
+ Прочитать объявленные проектом категории.
26
+ """
27
+ return tuple(getattr(settings, 'GLOBAL_CONFIG_CATEGORIES', ()))
28
+
29
+
30
+ def get_setting_definitions() -> Sequence[SettingDefinition]:
31
+ """
32
+ Прочитать объявленные проектом ключи.
33
+ """
34
+ return tuple(getattr(settings, 'GLOBAL_CONFIG_DEFINITIONS', ()))
35
+
36
+
37
+ def get_required_setting_keys() -> set[str]:
38
+ """
39
+ Получить системный состав обязательного полного снимка.
40
+ """
41
+ return {str(item.key) for item in get_setting_definitions()}
42
+
43
+
44
+ def get_encryption_key() -> str | None:
45
+ """
46
+ Прочитать ключ шифрования SECURE-значений.
47
+ """
48
+ key = getattr(settings, 'GLOBAL_CONFIG_ENCRYPTION_KEY', None)
49
+ if key is None or key == '':
50
+ return None
51
+ return str(key)
52
+
53
+
54
+ def _build_fernet(*, key: str) -> Fernet:
55
+ """
56
+ Построить Fernet из произвольной секретной строки.
57
+ """
58
+ digest = hashlib.sha256(key.encode('utf-8')).digest()
59
+ return Fernet(key=base64.urlsafe_b64encode(digest))
60
+
61
+
62
+ def encrypt_value(*, text: str) -> str:
63
+ """
64
+ Зашифровать SECURE-значение, если задан GLOBAL_CONFIG_ENCRYPTION_KEY.
65
+ """
66
+ key = get_encryption_key()
67
+ if key is None:
68
+ return text
69
+ return _build_fernet(key=key).encrypt(data=text.encode('utf-8')).decode('ascii')
70
+
71
+
72
+ def decrypt_value(*, text: str) -> str | None:
73
+ """
74
+ Расшифровать SECURE-значение, если задан GLOBAL_CONFIG_ENCRYPTION_KEY.
75
+ """
76
+ key = get_encryption_key()
77
+ if key is None:
78
+ return text
79
+ try:
80
+ return _build_fernet(key=key).decrypt(token=text.encode('ascii')).decode('utf-8')
81
+ except (InvalidToken, ValueError):
82
+ return None
83
+
84
+
85
+ def is_refresh_suppressed() -> bool:
86
+ """
87
+ Проверить, нужно ли пропустить post_save refresh.
88
+ """
89
+ return _REFRESH_SUPPRESSED.get()
90
+
91
+
92
+ @contextmanager
93
+ def suppress_cache_refresh() -> Any:
94
+ """
95
+ Подавить per-row refresh на время синхронизации definitions.
96
+ """
97
+ token: Token[bool] = _REFRESH_SUPPRESSED.set(True)
98
+ try:
99
+ yield
100
+ finally:
101
+ _REFRESH_SUPPRESSED.reset(token)
@@ -0,0 +1,4 @@
1
+ from django_gc.dto.definitions import CategoryDefinition, SettingDefinition
2
+ from django_gc.dto.global_config_cache_value import GlobalConfigCacheValueDTO
3
+
4
+ __all__ = ['CategoryDefinition', 'GlobalConfigCacheValueDTO', 'SettingDefinition']
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+ from typing import Any
4
+
5
+ from django_gc.enums import SettingType
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CategoryDefinition:
10
+ """
11
+ Описать системную категорию GlobalConfig для инициализации.
12
+ """
13
+
14
+ id: int
15
+ code: str
16
+ name: str
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SettingDefinition:
21
+ """
22
+ Описать управляемый кодом ключ GlobalConfig для инициализации.
23
+ """
24
+
25
+ key: str | StrEnum
26
+ description: str
27
+ default_value: Any
28
+ value_type: SettingType
29
+ category_id: int
30
+ nullable: bool = False
31
+ is_read_only: bool = False
32
+ variables: dict[str, Any] | None = None
33
+ is_always_update: bool = False
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from django_gc.enums import SettingType
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class GlobalConfigCacheValueDTO:
9
+ """
10
+ Типизированное значение снимка GlobalConfig.
11
+
12
+ Для SettingType.SECURE поле value содержит только зашифрованное значение.
13
+ """
14
+
15
+ value: Any
16
+ value_type: SettingType
@@ -0,0 +1,4 @@
1
+ from django_gc.enums.choices_enum import ChoicesEnumMixin
2
+ from django_gc.enums.setting_types import SettingType
3
+
4
+ __all__ = ['ChoicesEnumMixin', 'SettingType']
@@ -0,0 +1,36 @@
1
+ from enum import Enum
2
+
3
+ from django.utils.functional import Promise
4
+
5
+
6
+ class ChoicesEnumMixin(Enum):
7
+ """
8
+ Дать Django choices из членов enum с методом ``label()``.
9
+ """
10
+
11
+ def label(self) -> Promise:
12
+ """
13
+ Вернуть человекочитаемую подпись члена enum.
14
+ """
15
+ raise NotImplementedError
16
+
17
+ @classmethod
18
+ def values(cls) -> set[object]:
19
+ """
20
+ Вернуть множество значений членов.
21
+ """
22
+ return {item.value for item in cls}
23
+
24
+ @classmethod
25
+ def labels(cls) -> set[str]:
26
+ """
27
+ Вернуть множество строковых подписей членов.
28
+ """
29
+ return {str(item.label()) for item in cls}
30
+
31
+ @classmethod
32
+ def choices(cls) -> list[tuple[object, Promise]]:
33
+ """
34
+ Вернуть пары value/label для поля Django.
35
+ """
36
+ return [(member.value, member.label()) for member in cls]