documente_shared 0.1.145__py3-none-any.whl
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.
- documente_shared/__init__.py +0 -0
- documente_shared/application/__init__.py +0 -0
- documente_shared/application/dates.py +7 -0
- documente_shared/application/digest.py +7 -0
- documente_shared/application/exceptions.py +23 -0
- documente_shared/application/files.py +27 -0
- documente_shared/application/json.py +45 -0
- documente_shared/application/numbers.py +7 -0
- documente_shared/application/payloads.py +29 -0
- documente_shared/application/query_params.py +133 -0
- documente_shared/application/retry_utils.py +69 -0
- documente_shared/application/time_utils.py +13 -0
- documente_shared/application/timezone.py +7 -0
- documente_shared/domain/__init__.py +0 -0
- documente_shared/domain/base_enum.py +54 -0
- documente_shared/domain/constants.py +8 -0
- documente_shared/domain/entities/__init__.py +0 -0
- documente_shared/domain/entities/document.py +410 -0
- documente_shared/domain/entities/document_metadata.py +64 -0
- documente_shared/domain/entities/in_memory_document.py +75 -0
- documente_shared/domain/entities/processing_case.py +215 -0
- documente_shared/domain/entities/processing_case_filters.py +51 -0
- documente_shared/domain/entities/processing_case_item.py +300 -0
- documente_shared/domain/entities/processing_case_item_filters.py +54 -0
- documente_shared/domain/entities/processing_documents.py +11 -0
- documente_shared/domain/entities/processing_event.py +71 -0
- documente_shared/domain/entities/scaling.py +31 -0
- documente_shared/domain/enums/__init__.py +0 -0
- documente_shared/domain/enums/circular_oficio.py +29 -0
- documente_shared/domain/enums/common.py +133 -0
- documente_shared/domain/enums/document.py +124 -0
- documente_shared/domain/enums/document_type_record.py +13 -0
- documente_shared/domain/enums/processing_case.py +66 -0
- documente_shared/domain/exceptions.py +5 -0
- documente_shared/domain/interfaces/__init__.py +0 -0
- documente_shared/domain/interfaces/scaling.py +10 -0
- documente_shared/domain/repositories/__init__.py +0 -0
- documente_shared/domain/repositories/document.py +24 -0
- documente_shared/domain/repositories/processing_case.py +36 -0
- documente_shared/domain/repositories/processing_case_item.py +49 -0
- documente_shared/infrastructure/__init__.py +0 -0
- documente_shared/infrastructure/documente_client.py +27 -0
- documente_shared/infrastructure/dynamo_table.py +75 -0
- documente_shared/infrastructure/lambdas.py +14 -0
- documente_shared/infrastructure/repositories/__init__.py +0 -0
- documente_shared/infrastructure/repositories/dynamo_document.py +43 -0
- documente_shared/infrastructure/repositories/dynamo_processing_case.py +55 -0
- documente_shared/infrastructure/repositories/dynamo_processing_case_item.py +70 -0
- documente_shared/infrastructure/repositories/http_document.py +66 -0
- documente_shared/infrastructure/repositories/http_processing_case.py +82 -0
- documente_shared/infrastructure/repositories/http_processing_case_item.py +118 -0
- documente_shared/infrastructure/repositories/mem_document.py +46 -0
- documente_shared/infrastructure/repositories/mem_processing_case.py +44 -0
- documente_shared/infrastructure/repositories/mem_processing_case_item.py +52 -0
- documente_shared/infrastructure/s3_bucket.py +58 -0
- documente_shared/infrastructure/services/__init__.py +0 -0
- documente_shared/infrastructure/services/http_scaling.py +25 -0
- documente_shared/infrastructure/sqs_queue.py +48 -0
- documente_shared/presentation/__init__.py +0 -0
- documente_shared/presentation/presenters.py +16 -0
- documente_shared-0.1.145.dist-info/METADATA +39 -0
- documente_shared-0.1.145.dist-info/RECORD +63 -0
- documente_shared-0.1.145.dist-info/WHEEL +4 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, Any, TypeVar
|
|
4
|
+
|
|
5
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
6
|
+
|
|
7
|
+
def initialize_sentry(dsn: str, environment: str = 'dev') -> None:
|
|
8
|
+
if not sentry_sdk.Hub.current.client:
|
|
9
|
+
sentry_sdk.init(
|
|
10
|
+
dsn=dsn,
|
|
11
|
+
environment=environment,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def track_exceptions(func: F) -> F:
|
|
15
|
+
@wraps(func)
|
|
16
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
17
|
+
try:
|
|
18
|
+
return func(*args, **kwargs)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
sentry_sdk.capture_exception(e)
|
|
21
|
+
sentry_sdk.flush()
|
|
22
|
+
raise
|
|
23
|
+
return wrapper # type: ignore
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
from typing import Tuple, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def split_file_params(filepath: str) -> Tuple[str, str, str]:
|
|
6
|
+
folder_path = path.dirname(filepath)
|
|
7
|
+
filename = path.splitext(path.basename(filepath))[0]
|
|
8
|
+
extension = path.splitext(filepath)[1]
|
|
9
|
+
extension = extension.replace('.', '')
|
|
10
|
+
return folder_path, filename, extension
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_filename_from_path(file_path: Optional[str]) -> Optional[str]:
|
|
14
|
+
if not file_path:
|
|
15
|
+
return None
|
|
16
|
+
return path.basename(file_path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def remove_slash_from_path(file_path: str) -> str:
|
|
20
|
+
if file_path and file_path.startswith('/'):
|
|
21
|
+
return file_path[1:]
|
|
22
|
+
return file_path
|
|
23
|
+
|
|
24
|
+
def remove_extension(filename: str) -> str:
|
|
25
|
+
if filename and '.' in filename:
|
|
26
|
+
return filename.rsplit('.', 1)[0]
|
|
27
|
+
return filename
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import unicodedata
|
|
5
|
+
from unidecode import unidecode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def underscoreize(data: Any) -> Any:
|
|
9
|
+
if isinstance(data, dict):
|
|
10
|
+
new_dict = {}
|
|
11
|
+
for key, value in data.items():
|
|
12
|
+
new_key = re.sub(r"(?<!^)(?=[A-Z])", "_", key).lower()
|
|
13
|
+
new_dict[new_key] = underscoreize(value)
|
|
14
|
+
return new_dict
|
|
15
|
+
elif isinstance(data, list):
|
|
16
|
+
return [underscoreize(item) for item in data]
|
|
17
|
+
else:
|
|
18
|
+
return data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def safe_format(data: Any) -> Any:
|
|
22
|
+
if isinstance(data, dict):
|
|
23
|
+
new_dict = {}
|
|
24
|
+
for key, value in data.items():
|
|
25
|
+
new_key = unidecode(key.replace(" ", "_"))
|
|
26
|
+
new_dict[new_key] = safe_format(value)
|
|
27
|
+
return new_dict
|
|
28
|
+
elif isinstance(data, list):
|
|
29
|
+
return [safe_format(item) for item in data]
|
|
30
|
+
else:
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_key(key: str) -> str:
|
|
35
|
+
normalized = unicodedata.normalize('NFC', key)
|
|
36
|
+
underscored = re.sub(r'\s+', '_', normalized)
|
|
37
|
+
return underscored
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_dict_keys(data: dict) -> dict:
|
|
41
|
+
return {normalize_key(k): v for k, v in data.items()}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_list_keys(data: list) -> list:
|
|
45
|
+
return [normalize_dict_keys(item) for item in data]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def camel_to_snake_key(name: str) -> str:
|
|
5
|
+
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
6
|
+
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
7
|
+
|
|
8
|
+
def camel_to_snake(data: dict | list) -> dict | list:
|
|
9
|
+
if isinstance(data, dict):
|
|
10
|
+
return {camel_to_snake_key(k): camel_to_snake(v) for k, v in data.items()}
|
|
11
|
+
elif isinstance(data, list):
|
|
12
|
+
return [camel_to_snake(item) for item in data]
|
|
13
|
+
else:
|
|
14
|
+
return data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def snake_to_camel(data: dict | list) -> dict | list:
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
result = {}
|
|
20
|
+
for key, value in data.items():
|
|
21
|
+
parts = key.split("_")
|
|
22
|
+
camel_key = parts[0] + "".join(word.capitalize() for word in parts[1:])
|
|
23
|
+
result[camel_key] = snake_to_camel(value)
|
|
24
|
+
return result
|
|
25
|
+
elif isinstance(data, list):
|
|
26
|
+
return [snake_to_camel(item) for item in data]
|
|
27
|
+
else:
|
|
28
|
+
return data
|
|
29
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from documente_shared.domain.base_enum import BaseEnum
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Optional, Type, Union
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def camelize_string(value: str) -> str:
|
|
11
|
+
if not value:
|
|
12
|
+
return value
|
|
13
|
+
value = "".join(word.title() for word in value.split("_"))
|
|
14
|
+
return value[:1].lower() + value[1:]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_bool_to_str(input_value: bool) -> str:
|
|
18
|
+
return str(input_value).lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class QueryParams(object):
|
|
23
|
+
params: Union[dict]
|
|
24
|
+
|
|
25
|
+
def get(self, key, default=None):
|
|
26
|
+
return self.params.get(key, default) or self.params.get(camelize_string(key), default)
|
|
27
|
+
|
|
28
|
+
def get_datetime(self, key: str, default=None):
|
|
29
|
+
try:
|
|
30
|
+
value = self.get_str(key, default)
|
|
31
|
+
if not value:
|
|
32
|
+
return None
|
|
33
|
+
return datetime.strptime(value, "%Y-%m-%d")
|
|
34
|
+
except (ValueError, TypeError):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def get_str(self, key, default=None):
|
|
38
|
+
try:
|
|
39
|
+
return self._get_or_none(key, str, default)
|
|
40
|
+
except (ValueError, TypeError):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def get_int(self, key, default=None):
|
|
44
|
+
try:
|
|
45
|
+
return self._get_or_none(key, int, default)
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def get_float(self, key, default=None):
|
|
50
|
+
try:
|
|
51
|
+
return self._get_or_none(key, float, default)
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def get_bool(self, key, default=False):
|
|
56
|
+
try:
|
|
57
|
+
value = self.get(key, default)
|
|
58
|
+
if isinstance(value, bool):
|
|
59
|
+
return value
|
|
60
|
+
elif isinstance(value, str):
|
|
61
|
+
return value in ["true", "True", "TRUE", "1", "active"]
|
|
62
|
+
elif value is None:
|
|
63
|
+
return None
|
|
64
|
+
return False
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_uuid(self, key, default=None):
|
|
69
|
+
try:
|
|
70
|
+
return self._get_or_none(key, UUID, default)
|
|
71
|
+
except (ValueError, TypeError):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def get_uuid_list(self, key, default=None):
|
|
75
|
+
def parse_str(value):
|
|
76
|
+
uuid_list = []
|
|
77
|
+
for uuid_str in value.split(","):
|
|
78
|
+
try:
|
|
79
|
+
uuid_list.append(UUID(uuid_str))
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
return uuid_list
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return self._get_or_none(key, parse_str, default)
|
|
86
|
+
except (ValueError, TypeError):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def get_list(self, key, default=None):
|
|
90
|
+
default = default or []
|
|
91
|
+
try:
|
|
92
|
+
return self.params.getlist(key) or self.params.getlist(camelize_string(key)) or default
|
|
93
|
+
except (ValueError, TypeError):
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def get_enum(
|
|
97
|
+
self,
|
|
98
|
+
key: str,
|
|
99
|
+
enum_class: Type[BaseEnum],
|
|
100
|
+
default: BaseEnum = None,
|
|
101
|
+
) -> Optional[BaseEnum]:
|
|
102
|
+
try:
|
|
103
|
+
str_value = self.get_str(key) or default
|
|
104
|
+
return enum_class.from_value(str_value) if str_value else None
|
|
105
|
+
except (ValueError, TypeError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def get_enum_list(
|
|
109
|
+
self,
|
|
110
|
+
key: str,
|
|
111
|
+
enum_class: Type[BaseEnum],
|
|
112
|
+
default: BaseEnum = None,
|
|
113
|
+
) -> List:
|
|
114
|
+
str_value = self.get_str(key) or default
|
|
115
|
+
if not str_value:
|
|
116
|
+
return []
|
|
117
|
+
converted_enum_list = []
|
|
118
|
+
str_enum_items = str_value.split(",")
|
|
119
|
+
for str_enum_item in str_enum_items:
|
|
120
|
+
enum_value = enum_class.from_value(value=str_enum_item)
|
|
121
|
+
if not enum_value:
|
|
122
|
+
continue
|
|
123
|
+
converted_enum_list.append(enum_value)
|
|
124
|
+
return converted_enum_list
|
|
125
|
+
|
|
126
|
+
def _get_or_none(
|
|
127
|
+
self,
|
|
128
|
+
key: str,
|
|
129
|
+
cast_type: Type[Union[str, int, float, bool, UUID]],
|
|
130
|
+
default=None,
|
|
131
|
+
) -> Optional[Union[str, int, float, bool, UUID]]:
|
|
132
|
+
item_value = self.get(key, default)
|
|
133
|
+
return cast_type(item_value) if item_value else default
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Optional, Callable
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def retry_on_size_integrity(base_delay: float = 0.8, max_retries: int = 3):
|
|
9
|
+
def decorator(func: Callable):
|
|
10
|
+
@wraps(func)
|
|
11
|
+
def wrapper(self, file_key: str, *args, **kwargs) -> Optional[dict]:
|
|
12
|
+
for attempt in range(max_retries):
|
|
13
|
+
try:
|
|
14
|
+
file_context = func(self, file_key, *args, **kwargs)
|
|
15
|
+
|
|
16
|
+
if not file_context:
|
|
17
|
+
logger.warning(
|
|
18
|
+
f"[Intento {attempt+1}/{max_retries}] file_context es None para {file_key}"
|
|
19
|
+
)
|
|
20
|
+
if attempt < max_retries - 1:
|
|
21
|
+
delay = base_delay * (2 ** attempt) + random.uniform(0, base_delay)
|
|
22
|
+
time.sleep(delay)
|
|
23
|
+
continue
|
|
24
|
+
raise RuntimeError(f"No se pudo obtener file_context para {file_key}")
|
|
25
|
+
|
|
26
|
+
if 'Body' not in file_context:
|
|
27
|
+
logger.warning(
|
|
28
|
+
f"[Intento {attempt+1}/{max_retries}] 'Body' no encontrado en file_context para {file_key}"
|
|
29
|
+
)
|
|
30
|
+
if attempt < max_retries - 1:
|
|
31
|
+
delay = base_delay * (2 ** attempt) + random.uniform(0, base_delay)
|
|
32
|
+
time.sleep(delay)
|
|
33
|
+
continue
|
|
34
|
+
raise RuntimeError(f"'Body' no encontrado en file_context para {file_key}")
|
|
35
|
+
|
|
36
|
+
expected_size = file_context.get('ContentLength', 0)
|
|
37
|
+
body = file_context['Body'].read()
|
|
38
|
+
actual_size = len(body)
|
|
39
|
+
|
|
40
|
+
if expected_size == actual_size:
|
|
41
|
+
file_context['Body'] = body
|
|
42
|
+
if attempt > 0:
|
|
43
|
+
logger.info(f"Descarga exitosa de {file_key} después de {attempt+1} intentos")
|
|
44
|
+
return file_context
|
|
45
|
+
|
|
46
|
+
logger.warning(
|
|
47
|
+
f"[Intento {attempt+1}/{max_retries}] Discrepancia de tamaño para {file_key}: "
|
|
48
|
+
f"esperado={expected_size}, recibido={actual_size}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if attempt < max_retries - 1:
|
|
52
|
+
delay = base_delay * (2 ** attempt) + random.uniform(0, base_delay)
|
|
53
|
+
time.sleep(delay)
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(
|
|
57
|
+
f"[Intento {attempt+1}/{max_retries}] Error al descargar {file_key}: {type(e).__name__}: {str(e)}"
|
|
58
|
+
)
|
|
59
|
+
if attempt < max_retries - 1:
|
|
60
|
+
delay = base_delay * (2 ** attempt) + random.uniform(0, base_delay)
|
|
61
|
+
time.sleep(delay)
|
|
62
|
+
continue
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
f"Descarga incompleta/inconsistente de S3 para {file_key} después de {max_retries} intentos"
|
|
67
|
+
)
|
|
68
|
+
return wrapper
|
|
69
|
+
return decorator
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Union, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_datetime_from_data(input_datetime: Union[datetime, str]) -> Optional[datetime]:
|
|
6
|
+
if isinstance(input_datetime, datetime):
|
|
7
|
+
return input_datetime
|
|
8
|
+
elif isinstance(input_datetime, str) and bool(input_datetime):
|
|
9
|
+
try:
|
|
10
|
+
return datetime.fromisoformat(input_datetime)
|
|
11
|
+
except ValueError:
|
|
12
|
+
return None
|
|
13
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Union, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseEnum(Enum):
|
|
6
|
+
"""Provides the common functionalties to multiple model choices."""
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def get_members(cls):
|
|
10
|
+
return [tag for tag in cls if type(tag.value) in [int, str, float]]
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def choices(cls):
|
|
14
|
+
"""Generate choice options for models."""
|
|
15
|
+
return [
|
|
16
|
+
(option.value, option.value)
|
|
17
|
+
for option in cls
|
|
18
|
+
if type(option.value) in [int, str, float]
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def values(cls):
|
|
23
|
+
"""Returns values from choices."""
|
|
24
|
+
return [option.value for option in cls]
|
|
25
|
+
|
|
26
|
+
def __str__(self): # noqa: D105
|
|
27
|
+
return str(self.value)
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return self.__str__()
|
|
31
|
+
|
|
32
|
+
def __hash__(self):
|
|
33
|
+
return hash(self.value)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def as_list(cls):
|
|
37
|
+
"""Returns properties as a list."""
|
|
38
|
+
return [
|
|
39
|
+
value
|
|
40
|
+
for key, value in cls.__dict__.items()
|
|
41
|
+
if isinstance(value, str) and not key.startswith('__')
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_value(
|
|
46
|
+
cls,
|
|
47
|
+
value: Union[str, int],
|
|
48
|
+
) -> Optional['BaseEnum']:
|
|
49
|
+
for tag in cls:
|
|
50
|
+
if isinstance(tag.value, str) and str(tag.value).upper() == str(value).upper():
|
|
51
|
+
return tag
|
|
52
|
+
elif not isinstance(tag.value, str) and tag.value == value:
|
|
53
|
+
return tag
|
|
54
|
+
return None
|
|
File without changes
|