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.
Files changed (63) hide show
  1. documente_shared/__init__.py +0 -0
  2. documente_shared/application/__init__.py +0 -0
  3. documente_shared/application/dates.py +7 -0
  4. documente_shared/application/digest.py +7 -0
  5. documente_shared/application/exceptions.py +23 -0
  6. documente_shared/application/files.py +27 -0
  7. documente_shared/application/json.py +45 -0
  8. documente_shared/application/numbers.py +7 -0
  9. documente_shared/application/payloads.py +29 -0
  10. documente_shared/application/query_params.py +133 -0
  11. documente_shared/application/retry_utils.py +69 -0
  12. documente_shared/application/time_utils.py +13 -0
  13. documente_shared/application/timezone.py +7 -0
  14. documente_shared/domain/__init__.py +0 -0
  15. documente_shared/domain/base_enum.py +54 -0
  16. documente_shared/domain/constants.py +8 -0
  17. documente_shared/domain/entities/__init__.py +0 -0
  18. documente_shared/domain/entities/document.py +410 -0
  19. documente_shared/domain/entities/document_metadata.py +64 -0
  20. documente_shared/domain/entities/in_memory_document.py +75 -0
  21. documente_shared/domain/entities/processing_case.py +215 -0
  22. documente_shared/domain/entities/processing_case_filters.py +51 -0
  23. documente_shared/domain/entities/processing_case_item.py +300 -0
  24. documente_shared/domain/entities/processing_case_item_filters.py +54 -0
  25. documente_shared/domain/entities/processing_documents.py +11 -0
  26. documente_shared/domain/entities/processing_event.py +71 -0
  27. documente_shared/domain/entities/scaling.py +31 -0
  28. documente_shared/domain/enums/__init__.py +0 -0
  29. documente_shared/domain/enums/circular_oficio.py +29 -0
  30. documente_shared/domain/enums/common.py +133 -0
  31. documente_shared/domain/enums/document.py +124 -0
  32. documente_shared/domain/enums/document_type_record.py +13 -0
  33. documente_shared/domain/enums/processing_case.py +66 -0
  34. documente_shared/domain/exceptions.py +5 -0
  35. documente_shared/domain/interfaces/__init__.py +0 -0
  36. documente_shared/domain/interfaces/scaling.py +10 -0
  37. documente_shared/domain/repositories/__init__.py +0 -0
  38. documente_shared/domain/repositories/document.py +24 -0
  39. documente_shared/domain/repositories/processing_case.py +36 -0
  40. documente_shared/domain/repositories/processing_case_item.py +49 -0
  41. documente_shared/infrastructure/__init__.py +0 -0
  42. documente_shared/infrastructure/documente_client.py +27 -0
  43. documente_shared/infrastructure/dynamo_table.py +75 -0
  44. documente_shared/infrastructure/lambdas.py +14 -0
  45. documente_shared/infrastructure/repositories/__init__.py +0 -0
  46. documente_shared/infrastructure/repositories/dynamo_document.py +43 -0
  47. documente_shared/infrastructure/repositories/dynamo_processing_case.py +55 -0
  48. documente_shared/infrastructure/repositories/dynamo_processing_case_item.py +70 -0
  49. documente_shared/infrastructure/repositories/http_document.py +66 -0
  50. documente_shared/infrastructure/repositories/http_processing_case.py +82 -0
  51. documente_shared/infrastructure/repositories/http_processing_case_item.py +118 -0
  52. documente_shared/infrastructure/repositories/mem_document.py +46 -0
  53. documente_shared/infrastructure/repositories/mem_processing_case.py +44 -0
  54. documente_shared/infrastructure/repositories/mem_processing_case_item.py +52 -0
  55. documente_shared/infrastructure/s3_bucket.py +58 -0
  56. documente_shared/infrastructure/services/__init__.py +0 -0
  57. documente_shared/infrastructure/services/http_scaling.py +25 -0
  58. documente_shared/infrastructure/sqs_queue.py +48 -0
  59. documente_shared/presentation/__init__.py +0 -0
  60. documente_shared/presentation/presenters.py +16 -0
  61. documente_shared-0.1.145.dist-info/METADATA +39 -0
  62. documente_shared-0.1.145.dist-info/RECORD +63 -0
  63. documente_shared-0.1.145.dist-info/WHEEL +4 -0
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ import pytz
2
+ from datetime import datetime
3
+
4
+
5
+
6
+ def utc_now() -> datetime:
7
+ return datetime.now(tz=pytz.utc)
@@ -0,0 +1,7 @@
1
+ import hashlib
2
+
3
+
4
+ def get_file_digest(file_bytes: bytes) -> str:
5
+ sha256_hash = hashlib.sha256()
6
+ sha256_hash.update(file_bytes)
7
+ return sha256_hash.hexdigest()
@@ -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,7 @@
1
+ from decimal import Decimal
2
+
3
+
4
+ def normalize_number(number: str | float | Decimal) -> str:
5
+ if not isinstance(number, Decimal):
6
+ number = Decimal(number)
7
+ return str(number.quantize(Decimal('0.001')))
@@ -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
@@ -0,0 +1,7 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def ensure_timezone(dt: datetime, tz=timezone.utc) -> datetime:
5
+ if dt.tzinfo is None:
6
+ return dt.replace(tzinfo=tz)
7
+ return dt
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
@@ -0,0 +1,8 @@
1
+ import pytz
2
+ from os import environ
3
+
4
+
5
+ la_paz_tz = pytz.timezone("America/La_Paz")
6
+
7
+ DOCUMENTE_API_URL = environ.get("DOCUMENTE_API_URL")
8
+ DOCUMENTE_API_KEY = environ.get("DOCUMENTE_API_KEY")
File without changes