base-api-utils 1.1.51__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.
- base_api_utils-1.1.51/LICENSE +13 -0
- base_api_utils-1.1.51/PKG-INFO +95 -0
- base_api_utils-1.1.51/README.md +69 -0
- base_api_utils-1.1.51/pyproject.toml +40 -0
- base_api_utils-1.1.51/setup.cfg +4 -0
- base_api_utils-1.1.51/src/base_api_utils/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/domain/__init__.py +3 -0
- base_api_utils-1.1.51/src/base_api_utils/domain/event_message.py +28 -0
- base_api_utils-1.1.51/src/base_api_utils/domain/event_message_factory.py +34 -0
- base_api_utils-1.1.51/src/base_api_utils/domain/events.py +10 -0
- base_api_utils-1.1.51/src/base_api_utils/dtos/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/dtos/authz_user.py +26 -0
- base_api_utils-1.1.51/src/base_api_utils/fields/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/fields/email_list_field.py +82 -0
- base_api_utils-1.1.51/src/base_api_utils/filters/__init__.py +3 -0
- base_api_utils-1.1.51/src/base_api_utils/filters/base_filter.py +133 -0
- base_api_utils-1.1.51/src/base_api_utils/filters/custom_ordering_filter.py +16 -0
- base_api_utils-1.1.51/src/base_api_utils/filters/filter_utils_mixin.py +57 -0
- base_api_utils-1.1.51/src/base_api_utils/handlers/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/handlers/base_event_handler.py +14 -0
- base_api_utils-1.1.51/src/base_api_utils/ioc/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/ioc/container.py +14 -0
- base_api_utils-1.1.51/src/base_api_utils/models/__init__.py +1 -0
- base_api_utils-1.1.51/src/base_api_utils/models/item_image.py +13 -0
- base_api_utils-1.1.51/src/base_api_utils/security/__init__.py +11 -0
- base_api_utils-1.1.51/src/base_api_utils/security/abstract_access_token_service.py +11 -0
- base_api_utils-1.1.51/src/base_api_utils/security/abstract_oauth2_api_client.py +136 -0
- base_api_utils-1.1.51/src/base_api_utils/security/access_token_service.py +66 -0
- base_api_utils-1.1.51/src/base_api_utils/security/check_sponsor_user_permissions.py +75 -0
- base_api_utils-1.1.51/src/base_api_utils/security/group_required.py +79 -0
- base_api_utils-1.1.51/src/base_api_utils/security/oauth2_authentication.py +35 -0
- base_api_utils-1.1.51/src/base_api_utils/security/oauth2_client_factory.py +45 -0
- base_api_utils-1.1.51/src/base_api_utils/security/oauth2_scope_required.py +75 -0
- base_api_utils-1.1.51/src/base_api_utils/security/permission_event_handler.py +39 -0
- base_api_utils-1.1.51/src/base_api_utils/security/shared.py +50 -0
- base_api_utils-1.1.51/src/base_api_utils/security/user_permissions_service.py +151 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/__init__.py +5 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/base_model_serializer.py +194 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/email_list_char_field.py +45 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/nullable_slug_related_field.py +12 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/serializers_registry.py +28 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/task_result_serializer.py +7 -0
- base_api_utils-1.1.51/src/base_api_utils/serializers/timestamp_field.py +22 -0
- base_api_utils-1.1.51/src/base_api_utils/services/__init__.py +8 -0
- base_api_utils-1.1.51/src/base_api_utils/services/abstract_domain_events_service.py +15 -0
- base_api_utils-1.1.51/src/base_api_utils/services/abstract_event_dispatcher.py +10 -0
- base_api_utils-1.1.51/src/base_api_utils/services/abstract_images_service.py +18 -0
- base_api_utils-1.1.51/src/base_api_utils/services/abstract_storage_service.py +11 -0
- base_api_utils-1.1.51/src/base_api_utils/services/amqp_service.py +211 -0
- base_api_utils-1.1.51/src/base_api_utils/services/event_dispatcher.py +33 -0
- base_api_utils-1.1.51/src/base_api_utils/services/images_service.py +66 -0
- base_api_utils-1.1.51/src/base_api_utils/services/s3_service.py +57 -0
- base_api_utils-1.1.51/src/base_api_utils/services/strategies/__init__.py +2 -0
- base_api_utils-1.1.51/src/base_api_utils/services/strategies/abstract_task_runner_strategy.py +8 -0
- base_api_utils-1.1.51/src/base_api_utils/services/strategies/publish_domain_event_strategy.py +52 -0
- base_api_utils-1.1.51/src/base_api_utils/urls.py +8 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/__init__.py +12 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/async_helpers.py +43 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/circuit_breaker.py +45 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/config.py +20 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/custom_auto_schema.py +38 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/db_connection_helpers.py +26 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/enums.py +16 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/exceptions.py +94 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/external_errors.py +71 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/file_lock.py +98 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/oauth2_custom_schema_base.py +30 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/pagination.py +43 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/rate_utils.py +30 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/read_write_serializer_mixin.py +134 -0
- base_api_utils-1.1.51/src/base_api_utils/utils/string.py +77 -0
- base_api_utils-1.1.51/src/base_api_utils/views/__init__.py +4 -0
- base_api_utils-1.1.51/src/base_api_utils/views/base_view.py +42 -0
- base_api_utils-1.1.51/src/base_api_utils/views/parent_children_crud.py +97 -0
- base_api_utils-1.1.51/src/base_api_utils/views/root_entity_crud.py +85 -0
- base_api_utils-1.1.51/src/base_api_utils/views/tasks_result_view.py +36 -0
- base_api_utils-1.1.51/src/base_api_utils.egg-info/PKG-INFO +95 -0
- base_api_utils-1.1.51/src/base_api_utils.egg-info/SOURCES.txt +79 -0
- base_api_utils-1.1.51/src/base_api_utils.egg-info/dependency_links.txt +1 -0
- base_api_utils-1.1.51/src/base_api_utils.egg-info/requires.txt +13 -0
- base_api_utils-1.1.51/src/base_api_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2025 FNTECH
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: base-api-utils
|
|
3
|
+
Version: 1.1.51
|
|
4
|
+
Summary: Django Rest Framework micro services common utilities
|
|
5
|
+
Author-email: Román Gutierrez <roman@tipit.net>, Sebastian Marcet <sebastian@tipit.net>
|
|
6
|
+
Project-URL: Homepage, https://github.com/fntechgit/base-api-utils
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: boto3
|
|
13
|
+
Requires-Dist: celery
|
|
14
|
+
Requires-Dist: Django
|
|
15
|
+
Requires-Dist: django-celery-results
|
|
16
|
+
Requires-Dist: django_filter
|
|
17
|
+
Requires-Dist: djangorestframework>=3.15.2
|
|
18
|
+
Requires-Dist: drf-spectacular
|
|
19
|
+
Requires-Dist: injector
|
|
20
|
+
Requires-Dist: oauthlib
|
|
21
|
+
Requires-Dist: pika
|
|
22
|
+
Requires-Dist: pybreaker
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Requires-Dist: requests-oauthlib
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# base-api-utils
|
|
28
|
+
DRF common utilities
|
|
29
|
+
|
|
30
|
+
## Virtual Env
|
|
31
|
+
|
|
32
|
+
````bash
|
|
33
|
+
$ python3 -m venv env
|
|
34
|
+
|
|
35
|
+
$ source env/bin/activate
|
|
36
|
+
````
|
|
37
|
+
|
|
38
|
+
## python setup
|
|
39
|
+
|
|
40
|
+
````bash
|
|
41
|
+
sudo add-apt-repository ppa:deadsnakes/ppa -y
|
|
42
|
+
sudo apt-get -y -f install python3.7 python3-pip python3.7-dev python3.7-venv libpython3.7-dev python3-setuptools
|
|
43
|
+
sudo -H pip3 --default-timeout=50 install --upgrade pip
|
|
44
|
+
sudo -H pip3 install virtualenv
|
|
45
|
+
````
|
|
46
|
+
|
|
47
|
+
## Install reqs
|
|
48
|
+
|
|
49
|
+
````
|
|
50
|
+
pip install -r requirements.txt
|
|
51
|
+
````
|
|
52
|
+
|
|
53
|
+
## Packaging
|
|
54
|
+
|
|
55
|
+
Create ~/.pypirc:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
[distutils]
|
|
59
|
+
index-servers =
|
|
60
|
+
pypi
|
|
61
|
+
|
|
62
|
+
[pypi]
|
|
63
|
+
username = __token__
|
|
64
|
+
password = pypi-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
````bash
|
|
69
|
+
python3 -m pip install --upgrade build
|
|
70
|
+
python3 -m build
|
|
71
|
+
````
|
|
72
|
+
## Uploading
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python3 -m pip install --upgrade twine
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Test Py Pi
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python3 -m twine upload --repository testpypi dist/*
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Production PyPi
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python3 -m twine upload dist/*
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Install from testPyPi.Org
|
|
91
|
+
pip install -i https://test.pypi.org/simple/ base-api-utils --no-deps
|
|
92
|
+
|
|
93
|
+
## Install from GitHub
|
|
94
|
+
|
|
95
|
+
pip install git+https://github.com/fntechgit/base-api-utils
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# base-api-utils
|
|
2
|
+
DRF common utilities
|
|
3
|
+
|
|
4
|
+
## Virtual Env
|
|
5
|
+
|
|
6
|
+
````bash
|
|
7
|
+
$ python3 -m venv env
|
|
8
|
+
|
|
9
|
+
$ source env/bin/activate
|
|
10
|
+
````
|
|
11
|
+
|
|
12
|
+
## python setup
|
|
13
|
+
|
|
14
|
+
````bash
|
|
15
|
+
sudo add-apt-repository ppa:deadsnakes/ppa -y
|
|
16
|
+
sudo apt-get -y -f install python3.7 python3-pip python3.7-dev python3.7-venv libpython3.7-dev python3-setuptools
|
|
17
|
+
sudo -H pip3 --default-timeout=50 install --upgrade pip
|
|
18
|
+
sudo -H pip3 install virtualenv
|
|
19
|
+
````
|
|
20
|
+
|
|
21
|
+
## Install reqs
|
|
22
|
+
|
|
23
|
+
````
|
|
24
|
+
pip install -r requirements.txt
|
|
25
|
+
````
|
|
26
|
+
|
|
27
|
+
## Packaging
|
|
28
|
+
|
|
29
|
+
Create ~/.pypirc:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
[distutils]
|
|
33
|
+
index-servers =
|
|
34
|
+
pypi
|
|
35
|
+
|
|
36
|
+
[pypi]
|
|
37
|
+
username = __token__
|
|
38
|
+
password = pypi-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
````bash
|
|
43
|
+
python3 -m pip install --upgrade build
|
|
44
|
+
python3 -m build
|
|
45
|
+
````
|
|
46
|
+
## Uploading
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python3 -m pip install --upgrade twine
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Test Py Pi
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python3 -m twine upload --repository testpypi dist/*
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Production PyPi
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 -m twine upload dist/*
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Install from testPyPi.Org
|
|
65
|
+
pip install -i https://test.pypi.org/simple/ base-api-utils --no-deps
|
|
66
|
+
|
|
67
|
+
## Install from GitHub
|
|
68
|
+
|
|
69
|
+
pip install git+https://github.com/fntechgit/base-api-utils
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools.packages.find]
|
|
6
|
+
where = ["src"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "base-api-utils"
|
|
10
|
+
version = "1.1.51"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"boto3",
|
|
13
|
+
"celery",
|
|
14
|
+
"Django",
|
|
15
|
+
"django-celery-results",
|
|
16
|
+
"django_filter",
|
|
17
|
+
"djangorestframework>=3.15.2",
|
|
18
|
+
"drf-spectacular",
|
|
19
|
+
"injector",
|
|
20
|
+
"oauthlib",
|
|
21
|
+
"pika",
|
|
22
|
+
"pybreaker",
|
|
23
|
+
"requests",
|
|
24
|
+
"requests-oauthlib"
|
|
25
|
+
]
|
|
26
|
+
authors = [
|
|
27
|
+
{ name="Román Gutierrez", email="roman@tipit.net" },
|
|
28
|
+
{ name="Sebastian Marcet", email="sebastian@tipit.net" },
|
|
29
|
+
]
|
|
30
|
+
description = "Django Rest Framework micro services common utilities"
|
|
31
|
+
readme = "README.md"
|
|
32
|
+
requires-python = ">=3.10"
|
|
33
|
+
classifiers = [
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Operating System :: OS Independent",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/fntechgit/base-api-utils"
|
|
40
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .urls import private_tasks_admin_patterns
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Optional, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EventMessage:
|
|
6
|
+
def __init__(self,
|
|
7
|
+
event_type: str,
|
|
8
|
+
payload: Dict[str, Any],
|
|
9
|
+
routing_key: str,
|
|
10
|
+
properties: Optional[Dict[str, Any]] = None) -> None:
|
|
11
|
+
self.event_type = event_type
|
|
12
|
+
self.payload = payload
|
|
13
|
+
self.routing_key = routing_key
|
|
14
|
+
self.properties = properties
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def from_rabbit(ch, method, properties, body):
|
|
18
|
+
try:
|
|
19
|
+
payload = json.loads(body) if isinstance(body, (str, bytes)) else body
|
|
20
|
+
except json.JSONDecodeError:
|
|
21
|
+
payload = {"raw_body": str(body)}
|
|
22
|
+
|
|
23
|
+
return EventMessage(
|
|
24
|
+
event_type=method.routing_key,
|
|
25
|
+
payload=payload,
|
|
26
|
+
routing_key=method.routing_key,
|
|
27
|
+
properties=vars(properties) if properties else None
|
|
28
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
|
|
3
|
+
from . import EventMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EventMessageFactory:
|
|
7
|
+
"""Factory to create instances based on message type"""
|
|
8
|
+
|
|
9
|
+
_message_types: Dict[str, Type[EventMessage]] = {
|
|
10
|
+
"standard": EventMessage,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def register_type(cls, name: str, message_class: Type[EventMessage]):
|
|
15
|
+
cls._message_types[name] = message_class
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def create(cls, message_type: str, ch, method, properties, body) -> EventMessage:
|
|
19
|
+
"""
|
|
20
|
+
Creates an instance of the specified message type
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message_type: Message type name
|
|
24
|
+
ch, method, properties, body: RabbitMQ params
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
EventMessage instance or its subclass
|
|
28
|
+
"""
|
|
29
|
+
if message_type is None:
|
|
30
|
+
message_class = EventMessage
|
|
31
|
+
else:
|
|
32
|
+
message_class = cls._message_types.get(message_type, EventMessage)
|
|
33
|
+
|
|
34
|
+
return message_class.from_rabbit(ch, method, properties, body)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class DomainEvent(Enum):
|
|
4
|
+
AUTH_ACCESS_RIGHT_ADDED = "auth_access_right_added"
|
|
5
|
+
AUTH_ACCESS_RIGHT_REMOVED = "auth_access_right_removed"
|
|
6
|
+
AUTH_USER_ADDED_TO_GROUP = "auth_user_added_to_group"
|
|
7
|
+
AUTH_USER_REMOVED_FROM_GROUP = "auth_user_removed_from_group"
|
|
8
|
+
AUTH_USER_ADDED_TO_SPONSOR_AND_SUMMIT = "auth_user_added_to_sponsor_and_summit"
|
|
9
|
+
AUTH_USER_REMOVED_FROM_SPONSOR_AND_SUMMIT = "auth_user_removed_from_sponsor_and_summit"
|
|
10
|
+
AUTH_USER_REMOVED_FROM_SUMMIT = "auth_user_removed_from_summit"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .authz_user import AuthzUserDTO
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from ..utils import config
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthzUserDTO:
|
|
5
|
+
def __init__(self, token_info=None):
|
|
6
|
+
if token_info is None:
|
|
7
|
+
token_info = {}
|
|
8
|
+
self.id = token_info.get('user_id', None)
|
|
9
|
+
self.first_name = token_info.get('user_first_name', None)
|
|
10
|
+
self.last_name = token_info.get('user_last_name', None)
|
|
11
|
+
self.email = token_info.get('user_email', None)
|
|
12
|
+
self.groups = token_info.get('user_groups', [])
|
|
13
|
+
|
|
14
|
+
def is_admin(self) -> bool:
|
|
15
|
+
admin_groups = config('OAUTH2.ADMIN_GROUPS', '').split()
|
|
16
|
+
|
|
17
|
+
for group in self.groups:
|
|
18
|
+
if group.get("slug") in admin_groups:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .email_list_field import EmailListField
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from django.core.validators import validate_email
|
|
4
|
+
|
|
5
|
+
class EmailListField(models.CharField):
|
|
6
|
+
"""
|
|
7
|
+
Custom field that accepts a list of emails or a string with comma-separated emails.
|
|
8
|
+
Stores them as a SEPARATOR-separated string in the database.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SEPARATOR = "," # Separator constant for email list
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
# Set a default max_length if not specified
|
|
15
|
+
if 'max_length' not in kwargs:
|
|
16
|
+
kwargs['max_length'] = 1000
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
def deconstruct(self):
|
|
20
|
+
"""Required for Django migrations"""
|
|
21
|
+
name, path, args, kwargs = super().deconstruct()
|
|
22
|
+
return name, path, args, kwargs
|
|
23
|
+
|
|
24
|
+
def to_python(self, value):
|
|
25
|
+
"""Converts the value from the database to Python"""
|
|
26
|
+
if value is None:
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
if isinstance(value, list):
|
|
30
|
+
# If it comes as a list, validate each email and convert to string
|
|
31
|
+
validated_emails = []
|
|
32
|
+
for email in value:
|
|
33
|
+
email = str(email).strip()
|
|
34
|
+
if email:
|
|
35
|
+
try:
|
|
36
|
+
validate_email(email)
|
|
37
|
+
validated_emails.append(email)
|
|
38
|
+
except ValidationError:
|
|
39
|
+
raise ValidationError(f"'{email}' is not a valid email")
|
|
40
|
+
return self.SEPARATOR.join(validated_emails)
|
|
41
|
+
|
|
42
|
+
# If it's already a string, return as is
|
|
43
|
+
return super().to_python(value)
|
|
44
|
+
|
|
45
|
+
def get_prep_value(self, value):
|
|
46
|
+
"""Prepares the value to be saved to the database"""
|
|
47
|
+
if value is None:
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
if isinstance(value, list):
|
|
51
|
+
# If it comes as a list, convert to string
|
|
52
|
+
validated_emails = []
|
|
53
|
+
for email in value:
|
|
54
|
+
email = str(email).strip()
|
|
55
|
+
if email:
|
|
56
|
+
validated_emails.append(email)
|
|
57
|
+
return self.SEPARATOR.join(validated_emails)
|
|
58
|
+
|
|
59
|
+
return super().get_prep_value(value)
|
|
60
|
+
|
|
61
|
+
def validate(self, value, model_instance):
|
|
62
|
+
"""Custom field validation"""
|
|
63
|
+
# Call parent validation first
|
|
64
|
+
super().validate(value, model_instance)
|
|
65
|
+
|
|
66
|
+
if value:
|
|
67
|
+
# If the value is a list, it was already validated in to_python
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
# Validate each email in the string
|
|
70
|
+
emails = [email.strip() for email in value.split(self.SEPARATOR)]
|
|
71
|
+
for email in emails:
|
|
72
|
+
if email: # Avoid empty strings
|
|
73
|
+
try:
|
|
74
|
+
validate_email(email)
|
|
75
|
+
except ValidationError:
|
|
76
|
+
raise ValidationError(f"'{email}' is not a valid email")
|
|
77
|
+
|
|
78
|
+
def get_emails_list(self, value):
|
|
79
|
+
"""Helper method to get the list of emails from the stored value"""
|
|
80
|
+
if not value:
|
|
81
|
+
return []
|
|
82
|
+
return [email.strip() for email in value.split(self.SEPARATOR.strip()) if email.strip()]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from .filter_utils_mixin import FilterUtilsMixin
|
|
2
|
+
from ..utils import safe_str_to_number
|
|
3
|
+
from django.db.models import Q
|
|
4
|
+
from django_filters import rest_framework as filters
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseFilter(FilterUtilsMixin, filters.FilterSet):
|
|
8
|
+
def parse_list_values(self, value_str):
|
|
9
|
+
"""
|
|
10
|
+
Parses a string with values separated by && and returns a list.
|
|
11
|
+
Automatically converts to numbers if possible.
|
|
12
|
+
"""
|
|
13
|
+
if self.list_separator not in value_str:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
values = []
|
|
17
|
+
for val in value_str.split(self.list_separator):
|
|
18
|
+
val = val.strip()
|
|
19
|
+
if self.is_numeric(val):
|
|
20
|
+
values.append(safe_str_to_number(val))
|
|
21
|
+
elif self.is_boolean(val):
|
|
22
|
+
values.append(str(val).lower() == 'true')
|
|
23
|
+
else:
|
|
24
|
+
values.append(val)
|
|
25
|
+
return values
|
|
26
|
+
|
|
27
|
+
def filter_field(self, queryset, filter_name, filter_op, filter_value):
|
|
28
|
+
"""
|
|
29
|
+
Generic method for filtering based on dynamic comparisons.
|
|
30
|
+
"""
|
|
31
|
+
if filter_op is None:
|
|
32
|
+
return queryset
|
|
33
|
+
|
|
34
|
+
is_exclude_filter = filter_name.endswith('__not_in')
|
|
35
|
+
|
|
36
|
+
if is_exclude_filter:
|
|
37
|
+
# Remove the _not_in suffix to get the actual field name
|
|
38
|
+
base_field_name = filter_name[:-8] # Remove '__not_in'
|
|
39
|
+
|
|
40
|
+
# Parse the values in the list
|
|
41
|
+
list_values = self.parse_list_values(filter_value)
|
|
42
|
+
|
|
43
|
+
if list_values:
|
|
44
|
+
# Apply exclusion with __in
|
|
45
|
+
return queryset.exclude(**{f"{base_field_name}__in": list_values})
|
|
46
|
+
else:
|
|
47
|
+
# If not a list, apply simple exclusion
|
|
48
|
+
value_to_filter = (
|
|
49
|
+
safe_str_to_number(filter_value)
|
|
50
|
+
if self.is_numeric(filter_value)
|
|
51
|
+
else filter_value
|
|
52
|
+
)
|
|
53
|
+
return queryset.exclude(**{f"{base_field_name}{filter_op}": value_to_filter})
|
|
54
|
+
|
|
55
|
+
value_to_filter = (
|
|
56
|
+
safe_str_to_number(filter_value)) \
|
|
57
|
+
if filter_op in ['__gte', '__lte', '__gt', '__lt', ''] and self.is_numeric(filter_value) \
|
|
58
|
+
else filter_value
|
|
59
|
+
|
|
60
|
+
if self.is_boolean(value_to_filter):
|
|
61
|
+
value_to_filter = str(value_to_filter).lower() == 'true'
|
|
62
|
+
|
|
63
|
+
return queryset.filter(**{f"{filter_name}{filter_op}": value_to_filter})
|
|
64
|
+
|
|
65
|
+
def apply_or_filters(self, queryset, or_filters):
|
|
66
|
+
or_filter_subquery = Q()
|
|
67
|
+
active_filters = self.get_filters()
|
|
68
|
+
for or_filter in or_filters:
|
|
69
|
+
filter_name, filter_op, filter_value = self.parse_filter(or_filter)
|
|
70
|
+
if filter_name in active_filters:
|
|
71
|
+
filter_name = active_filters[filter_name].field_name
|
|
72
|
+
or_filter_subquery |= Q(**{f"{filter_name}{filter_op}": filter_value})
|
|
73
|
+
queryset = queryset.filter(or_filter_subquery)
|
|
74
|
+
return queryset
|
|
75
|
+
|
|
76
|
+
def filter_queryset(self, queryset):
|
|
77
|
+
"""
|
|
78
|
+
Overrides the `filter_queryset` method to apply filters dynamically.
|
|
79
|
+
Invoked when Django applies filters to the query.
|
|
80
|
+
|
|
81
|
+
Examples of possible filters:
|
|
82
|
+
- filter[]=field1=@value1,field2>value2 (OR filter)
|
|
83
|
+
|
|
84
|
+
- filter=field1=@value1,field2>value2 (OR filter)
|
|
85
|
+
|
|
86
|
+
- filter[]=field1=@value1&filter[]=field2>value2 (AND filter)
|
|
87
|
+
|
|
88
|
+
- filter==field1@@value
|
|
89
|
+
|
|
90
|
+
- filter[]=field1_not_in==10&&20&&30 (Exclude list)
|
|
91
|
+
|
|
92
|
+
- filter[]=field1=@value1,field2=@value2&filter[]=field3=@value3 (OR then AND filter)
|
|
93
|
+
Generates: ...WHERE (field1 LIKE '%value1%' OR field2 LIKE '%value2%') AND field3 LIKE '%value3%'
|
|
94
|
+
"""
|
|
95
|
+
and_filter_key = 'and'
|
|
96
|
+
or_filter_key = 'or'
|
|
97
|
+
|
|
98
|
+
filter_without_brackets = self.data.get(self.filter_param, None)
|
|
99
|
+
filter_with_brackets = self.data.getlist(self.filters_param, [])
|
|
100
|
+
|
|
101
|
+
filter_category = {
|
|
102
|
+
or_filter_key: filter_without_brackets.split(self.filters_separator) if filter_without_brackets else [],
|
|
103
|
+
and_filter_key: []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for item in filter_with_brackets:
|
|
107
|
+
if "," in item:
|
|
108
|
+
filter_category[or_filter_key].extend(item.split(self.filters_separator))
|
|
109
|
+
else:
|
|
110
|
+
filter_category[and_filter_key].append(item)
|
|
111
|
+
|
|
112
|
+
# Apply dynamic filters
|
|
113
|
+
if filter_category[or_filter_key]:
|
|
114
|
+
queryset = self.apply_or_filters(queryset, filter_category[or_filter_key])
|
|
115
|
+
|
|
116
|
+
if not filter_category[and_filter_key]:
|
|
117
|
+
return queryset
|
|
118
|
+
|
|
119
|
+
# Get filters defined in the FilterSet
|
|
120
|
+
active_filters = self.get_filters()
|
|
121
|
+
|
|
122
|
+
for and_filter in filter_category[and_filter_key]:
|
|
123
|
+
filter_name, filter_op, filter_value = self.parse_filter(and_filter)
|
|
124
|
+
if filter_name in active_filters:
|
|
125
|
+
filter_instance = active_filters[filter_name]
|
|
126
|
+
|
|
127
|
+
if filter_instance.method == 'epoch_to_datetime':
|
|
128
|
+
queryset = self.filter_by_epoch_timestamp(queryset, filter_instance.field_name, filter_op, filter_value)
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
queryset = self.filter_field(queryset, filter_instance.field_name, filter_op, filter_value)
|
|
132
|
+
|
|
133
|
+
return queryset
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from rest_framework.filters import OrderingFilter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CustomOrderingFilter(OrderingFilter):
|
|
5
|
+
def get_ordering(self, request, queryset, view):
|
|
6
|
+
ordering = super().get_ordering(request, queryset, view)
|
|
7
|
+
if ordering:
|
|
8
|
+
ordering_map = getattr(view, 'ordering_fields', {})
|
|
9
|
+
|
|
10
|
+
mapped_ordering = []
|
|
11
|
+
for field in ordering:
|
|
12
|
+
desc = '-' if field.startswith('-') else ''
|
|
13
|
+
clean_field = field.lstrip('-')
|
|
14
|
+
mapped_ordering.append(desc + ordering_map.get(clean_field, clean_field))
|
|
15
|
+
return mapped_ordering
|
|
16
|
+
return ordering
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from rest_framework.exceptions import ValidationError
|
|
4
|
+
|
|
5
|
+
class FilterUtilsMixin:
|
|
6
|
+
"""Mixin with utilities for filtering and converting values."""
|
|
7
|
+
|
|
8
|
+
filter_op = '='
|
|
9
|
+
filters_separator = ','
|
|
10
|
+
name_operator_separator = '__'
|
|
11
|
+
filter_param = 'filter'
|
|
12
|
+
filters_param = 'filter[]'
|
|
13
|
+
list_separator = '&&'
|
|
14
|
+
|
|
15
|
+
operator_map = {
|
|
16
|
+
'>=': "__gte",
|
|
17
|
+
'<=': "__lte",
|
|
18
|
+
'>': "__gt",
|
|
19
|
+
'<': "__lt",
|
|
20
|
+
'==': "",
|
|
21
|
+
'=@': "__icontains",
|
|
22
|
+
'@@': "__istartswith"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
self.is_number_re = re.compile(r'^-?\d+(\.\d+)?([eE][-+]?\d+)?$')
|
|
28
|
+
self.split_pattern = '|'.join(map(re.escape, self.operator_map.keys()))
|
|
29
|
+
|
|
30
|
+
def is_numeric(self, value):
|
|
31
|
+
return bool(self.is_number_re.match(str(value)))
|
|
32
|
+
|
|
33
|
+
def is_boolean(self, value):
|
|
34
|
+
return str(value).lower() in ['true', 'false']
|
|
35
|
+
|
|
36
|
+
def epoch_to_datetime(self, value):
|
|
37
|
+
try:
|
|
38
|
+
return datetime.fromtimestamp(int(value), tz=timezone.utc)
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def filter_by_epoch_timestamp(self, queryset, name, op, value):
|
|
43
|
+
dt = self.epoch_to_datetime(value)
|
|
44
|
+
if dt is not None:
|
|
45
|
+
return queryset.filter(**{f"{name}{op}": dt})
|
|
46
|
+
else:
|
|
47
|
+
return queryset.none()
|
|
48
|
+
|
|
49
|
+
def parse_filter(self, filter_str):
|
|
50
|
+
try:
|
|
51
|
+
parts = re.split(f"({self.split_pattern})", filter_str)
|
|
52
|
+
field_name = parts[0].strip()
|
|
53
|
+
operator = self.operator_map.get(parts[1].strip())
|
|
54
|
+
value = parts[2].strip()
|
|
55
|
+
except ValueError:
|
|
56
|
+
raise ValidationError("Invalid filter format. Expected 'key=value'.")
|
|
57
|
+
return field_name, operator, value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base_event_handler import BaseEventHandler
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from ..domain import EventMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseEventHandler(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def can_handle(self, event_type: str) -> bool:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def handle(self, event: EventMessage) -> bool:
|
|
14
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .container import create_container, get_container, set_container
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from injector import Module, Injector
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
def create_container(modules: List[Module]) -> Injector:
|
|
5
|
+
return Injector(modules)
|
|
6
|
+
|
|
7
|
+
_container: Optional[Injector] = None
|
|
8
|
+
|
|
9
|
+
def get_container() -> Optional[Injector]:
|
|
10
|
+
return _container
|
|
11
|
+
|
|
12
|
+
def set_container(container: Injector):
|
|
13
|
+
global _container
|
|
14
|
+
_container = container
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .item_image import ItemImage
|