documente_shared 0.1.202__tar.gz → 0.1.203__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 (133) hide show
  1. {documente_shared-0.1.202 → documente_shared-0.1.203}/PKG-INFO +1 -1
  2. documente_shared-0.1.203/PR_DESCRIPTION.md +46 -0
  3. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/layout.py +6 -0
  4. documente_shared-0.1.203/documente_shared/infrastructure/lambda_job_results.py +59 -0
  5. documente_shared-0.1.203/documente_shared/infrastructure/lambdas.py +35 -0
  6. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/s3_bucket.py +19 -1
  7. {documente_shared-0.1.202 → documente_shared-0.1.203}/pyproject.toml +1 -1
  8. {documente_shared-0.1.202 → documente_shared-0.1.203}/uv.lock +25 -25
  9. documente_shared-0.1.202/documente_shared/infrastructure/lambdas.py +0 -15
  10. {documente_shared-0.1.202 → documente_shared-0.1.203}/.claude/commands/commit.md +0 -0
  11. {documente_shared-0.1.202 → documente_shared-0.1.203}/.github/workflows/publish.yml +0 -0
  12. {documente_shared-0.1.202 → documente_shared-0.1.203}/.gitignore +0 -0
  13. {documente_shared-0.1.202 → documente_shared-0.1.203}/.vscode/settings.json +0 -0
  14. {documente_shared-0.1.202 → documente_shared-0.1.203}/Dockerfile +0 -0
  15. {documente_shared-0.1.202 → documente_shared-0.1.203}/Makefile +0 -0
  16. {documente_shared-0.1.202 → documente_shared-0.1.203}/README.md +0 -0
  17. {documente_shared-0.1.202 → documente_shared-0.1.203}/__init__.py +0 -0
  18. {documente_shared-0.1.202 → documente_shared-0.1.203}/cases.py +0 -0
  19. {documente_shared-0.1.202 → documente_shared-0.1.203}/docker-compose.yml +0 -0
  20. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/__init__.py +0 -0
  21. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/__init__.py +0 -0
  22. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/dates.py +0 -0
  23. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/digest.py +0 -0
  24. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/exceptions.py +0 -0
  25. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/files.py +0 -0
  26. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/json.py +0 -0
  27. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/logging.py +0 -0
  28. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/numbers.py +0 -0
  29. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/payloads.py +0 -0
  30. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/query_params.py +0 -0
  31. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/retry_utils.py +0 -0
  32. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/time_utils.py +0 -0
  33. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/application/timezone.py +0 -0
  34. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/__init__.py +0 -0
  35. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/base_enum.py +0 -0
  36. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/constants.py +0 -0
  37. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/__init__.py +0 -0
  38. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/document.py +0 -0
  39. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/document_metadata.py +0 -0
  40. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/document_type.py +0 -0
  41. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/in_memory_document.py +0 -0
  42. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_case.py +0 -0
  43. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_case_filters.py +0 -0
  44. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_case_item.py +0 -0
  45. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_case_item_filters.py +0 -0
  46. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_documents.py +0 -0
  47. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_event.py +0 -0
  48. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/processing_record.py +0 -0
  49. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/scaling.py +0 -0
  50. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/uif_registry.py +0 -0
  51. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/workspace.py +0 -0
  52. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/workspace_document.py +0 -0
  53. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/entities/workspace_document_page.py +0 -0
  54. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/__init__.py +0 -0
  55. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/circular_oficio.py +0 -0
  56. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/common.py +0 -0
  57. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/document.py +0 -0
  58. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/document_type_record.py +0 -0
  59. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/processing_case.py +0 -0
  60. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/processing_case_validator.py +0 -0
  61. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/vnext.py +0 -0
  62. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/enums/workspace.py +0 -0
  63. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/exceptions.py +0 -0
  64. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/filters/__init__.py +0 -0
  65. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/filters/document_type.py +0 -0
  66. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/filters/workspace.py +0 -0
  67. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/filters/workspace_document.py +0 -0
  68. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/filters/workspace_document_page.py +0 -0
  69. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/helpers/__init__.py +0 -0
  70. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/helpers/dicts.py +0 -0
  71. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/helpers/encoding.py +0 -0
  72. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/helpers/lists.py +0 -0
  73. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/helpers/values.py +0 -0
  74. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/interfaces/__init__.py +0 -0
  75. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/interfaces/queue.py +0 -0
  76. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/interfaces/scaling.py +0 -0
  77. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/pagination/__init__.py +0 -0
  78. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/pagination/entities.py +0 -0
  79. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/pagination/response.py +0 -0
  80. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/__init__.py +0 -0
  81. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/document.py +0 -0
  82. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/document_type.py +0 -0
  83. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/processing_case.py +0 -0
  84. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/processing_case_item.py +0 -0
  85. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/processing_record.py +0 -0
  86. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/uif_registry.py +0 -0
  87. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/workspace.py +0 -0
  88. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/workspace_document.py +0 -0
  89. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/domain/repositories/workspace_document_page.py +0 -0
  90. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/__init__.py +0 -0
  91. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/batch_queue.py +0 -0
  92. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/documente_client.py +0 -0
  93. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/dynamo_table.py +0 -0
  94. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/__init__.py +0 -0
  95. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/dynamo_document.py +0 -0
  96. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/dynamo_processing_case.py +0 -0
  97. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/dynamo_processing_case_item.py +0 -0
  98. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_document.py +0 -0
  99. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_document_type.py +0 -0
  100. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_processing_case.py +0 -0
  101. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_processing_case_item.py +0 -0
  102. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_processing_record.py +0 -0
  103. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_uif_registry.py +0 -0
  104. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_workspace.py +0 -0
  105. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_workspace_document.py +0 -0
  106. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/http_workspace_document_page.py +0 -0
  107. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/mem_document.py +0 -0
  108. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/mem_processing_case.py +0 -0
  109. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/mem_processing_case_item.py +0 -0
  110. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/repositories/mem_processing_record.py +0 -0
  111. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/services/__init__.py +0 -0
  112. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/services/http_scaling.py +0 -0
  113. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/infrastructure/sqs_queue.py +0 -0
  114. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/presentation/__init__.py +0 -0
  115. {documente_shared-0.1.202 → documente_shared-0.1.203}/documente_shared/presentation/presenters.py +0 -0
  116. {documente_shared-0.1.202 → documente_shared-0.1.203}/publish.sh +0 -0
  117. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/__init__.py +0 -0
  118. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/__init__.py +0 -0
  119. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/application/__init__.py +0 -0
  120. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/application/test_files.py +0 -0
  121. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/application/test_payloads.py +0 -0
  122. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/conftest.py +0 -0
  123. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/__init__.py +0 -0
  124. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/entities/__init__.py +0 -0
  125. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/entities/test_in_memory_document.py +0 -0
  126. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/entities/test_processing_case.py +0 -0
  127. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/entities/test_processing_case_item.py +0 -0
  128. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/entities/test_processing_record.py +0 -0
  129. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/domain/enums/test_processing_case_validator.py +0 -0
  130. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/infrastructure/__init__.py +0 -0
  131. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/infrastructure/repositories/__init__.py +0 -0
  132. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documente_shared/infrastructure/repositories/test_http_processing_record.py +0 -0
  133. {documente_shared-0.1.202 → documente_shared-0.1.203}/tests/documents.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: documente_shared
3
- Version: 0.1.202
3
+ Version: 0.1.203
4
4
  Summary: Shared utilities for LlamitAI projects
5
5
  Author-email: Tech <tech@llamitai.com>
6
6
  License: MIT
@@ -0,0 +1,46 @@
1
+ ## Summary
2
+
3
+ `invoke_lambda` construía un cliente boto3 nuevo en cada llamada, con 60s de `read_timeout` y los reintentos por defecto de botocore. Contra lambdas largas eso producía invocaciones duplicadas: la conexión moría en el VPC endpoint, el cliente esperaba de más y reintentaba, ejecutando la misma función hasta 5 veces. Este PR corrige la configuración del cliente y agrega las primitivas para recuperar el resultado desde S3 cuando la conexión se corta.
4
+
5
+ ## What Changed
6
+
7
+ **`infrastructure/lambdas.py`**
8
+ - `read_timeout` por defecto de 60 → 350, alineado al idle timeout del VPC endpoint
9
+ - `retries` explícito: modo `standard` con 1 reintento, en vez del `legacy` con 5 que regía por omisión
10
+ - `connect_timeout` de 30 y `tcp_keepalive`
11
+ - El cliente se cachea por configuración (`functools.cache`) en vez de construirse en cada invocación
12
+
13
+ **`infrastructure/lambda_job_results.py`** (nuevo)
14
+ - `build_job_result_key(job_id, step_name)`: reconstruye el `jobs/{job_id}/{step_name}.json` que usan las lambdas, para no depender de que la respuesta traiga el `output_uri`
15
+ - `wait_for_job_result(...)`: espera ese archivo con timeout y backoff fijo
16
+ - `await_job_result(...)`: la operación completa que usan los consumidores — valida `job_id`, arma el key, loguea y espera
17
+
18
+ **`infrastructure/s3_bucket.py`**
19
+ - `get_json()`: lectura donde la ausencia del objeto devuelve `None` en vez de ser un error
20
+
21
+ **`domain/entities/layout.py`**
22
+ - `PageRange.label`: la property que las lambdas ya usan para nombrar su salida en S3
23
+
24
+ ## Why
25
+
26
+ Las invocaciones salen por un Interface VPC Endpoint, que descarta los flujos TCP ociosos a los **350 segundos**. Una invocación sincrónica deja el flujo completamente ocioso mientras la lambda trabaja, así que cualquier función que pase de ese umbral pierde la conexión.
27
+
28
+ El corte es **silencioso**: el NLB no manda nada, y el RST aparece solo si alguien escribe sobre la conexión muerta — cosa que un cliente que solo espera la respuesta nunca hace. Así que boto3 no se entera hasta que vence su propio `read_timeout`, y ahí reintenta.
29
+
30
+ Sin bloque `retries` rige el modo `legacy`, que son **5 intentos**, y `ReadTimeoutError` es reintentable. El resultado era hasta 5 ejecuciones de la misma función no idempotente.
31
+
32
+ **Sobre los valores elegidos:**
33
+
34
+ - **`read_timeout=350`**: esperar más solo demora una falla que ya ocurrió, porque pasado ese punto el flujo está muerto y la respuesta no puede llegar.
35
+ - **`max_attempts=1`**: un read timeout ahora significa *la invocación sigue corriendo del otro lado*, así que reintentar la duplica. Se probó `0`, pero con ese valor la lambda no llegaba a ejecutarse en el entorno real.
36
+ - **Cliente cacheado**: construir un cliente carga los modelos del servicio y arma un connection pool nuevo, así que un cliente descartable pagaba un handshake TLS completo por invocación — justo el tramo de red que venía fallando.
37
+
38
+ **Sobre `get_json`:** `get_bytes` no sirve para preguntar si un objeto existe. Su decorador `retry_on_size_integrity` trata la ausencia como un error a reintentar: duerme y termina levantando `RuntimeError`. `get_json` deja la ausencia como respuesta válida, que es lo que necesita un poll.
39
+
40
+ **Sobre `PageRange.label`:** la copia de la entidad en vnext-tools ya tenía esta property y la usa para nombrar el archivo de salida. Agregarla acá hace que las dos copias vuelvan a coincidir, en vez de que cada consumidor replique la derivación.
41
+
42
+ ## Notas para el reviewer
43
+
44
+ - **La versión quedó en `0.1.203`, no `0.1.203b`.** El pin actual de `documente-core` es `documente-shared==0.1.203b`, que no matchea una versión estable. Hay que alinear los dos antes de mergear.
45
+ - La versión `0.1.203b0` ya publicada **no** contiene estos cambios: se publicó desde un árbol anterior. Hace falta publicar de nuevo.
46
+ - El diagnóstico completo, con los valores medidos y las alternativas descartadas (entre ellas TCP keepalive por sysctl, que infraestructura probó y no resuelve el corte), está en `docs/lambda-extractores-timeout-350s.md` del PR de `documente-core`.
@@ -75,3 +75,9 @@ class PageRange(BaseModel):
75
75
  end: Optional[int] = None
76
76
 
77
77
  model_config = ConfigDict(extra="ignore")
78
+
79
+ @property
80
+ def label(self) -> str:
81
+ start_page = self.start if self.start is not None else 1
82
+ end_page = self.end if self.end is not None else "end"
83
+ return f"{start_page}-{end_page}"
@@ -0,0 +1,59 @@
1
+ import time
2
+
3
+ import structlog
4
+
5
+ from documente_shared.infrastructure.s3_bucket import S3Bucket
6
+
7
+ logger = structlog.get_logger()
8
+
9
+ DEFAULT_JOB_RESULT_TIMEOUT = 960
10
+ DEFAULT_JOB_RESULT_POLL_INTERVAL = 5
11
+
12
+
13
+ def build_job_result_key(job_id: str, step_name: str) -> str:
14
+ return f"jobs/{job_id}/{step_name}.json"
15
+
16
+
17
+ def wait_for_job_result(
18
+ s3_bucket: S3Bucket,
19
+ job_result_key: str,
20
+ timeout_seconds: int = DEFAULT_JOB_RESULT_TIMEOUT,
21
+ poll_interval_seconds: float = DEFAULT_JOB_RESULT_POLL_INTERVAL,
22
+ ) -> dict:
23
+ deadline = time.monotonic() + timeout_seconds
24
+ while True:
25
+ job_result = s3_bucket.get_json(file_key=job_result_key)
26
+ if job_result is not None:
27
+ return job_result
28
+ if time.monotonic() >= deadline:
29
+ timeout_message = (
30
+ f"Lambda job result not found after {timeout_seconds}s: "
31
+ f"s3://{s3_bucket.bucket_name}/{job_result_key}"
32
+ )
33
+ raise TimeoutError(timeout_message)
34
+ time.sleep(poll_interval_seconds)
35
+
36
+
37
+ def await_job_result(
38
+ s3_bucket: S3Bucket,
39
+ job_id: str,
40
+ step_name: str,
41
+ connection_error: Exception,
42
+ ) -> tuple[str, dict]:
43
+ """La conexion murio en el VPC endpoint, que corta los flujos ociosos a los 350s y lo hace en
44
+ silencio. La lambda del otro lado sigue corriendo y va a escribir su resultado igual, asi
45
+ que en vez de reinvocarla se espera ese archivo en el key determinstico."""
46
+
47
+ if not job_id:
48
+ missing_job_id_message = "job_id es obligatorio para recuperar el resultado desde S3."
49
+ raise ValueError(missing_job_id_message)
50
+
51
+ job_result_key = build_job_result_key(job_id=job_id, step_name=step_name)
52
+ logger.warning(
53
+ "[shared.lambda_job_result.awaiting]",
54
+ job_id=job_id,
55
+ job_result_key=job_result_key,
56
+ reason=type(connection_error).__name__,
57
+ )
58
+ job_result = wait_for_job_result(s3_bucket=s3_bucket, job_result_key=job_result_key)
59
+ return job_result_key, job_result
@@ -0,0 +1,35 @@
1
+ import json
2
+ from functools import cache
3
+
4
+ import boto3
5
+ from botocore.config import Config
6
+
7
+ # Las invocaciones salen por un VPC endpoint, que descarta los flujos ociosos a los 350s
8
+ DEFAULT_LAMBDA_READ_TIMEOUT = 350
9
+
10
+
11
+ @cache
12
+ def _get_lambda_client(read_timeout: int):
13
+ lambda_config = Config(
14
+ read_timeout=read_timeout,
15
+ connect_timeout=30,
16
+ tcp_keepalive=True,
17
+ retries={"max_attempts": 1, "mode": "standard"},
18
+ )
19
+ return boto3.client("lambda", config=lambda_config)
20
+
21
+
22
+ def invoke_lambda(
23
+ function_name: str,
24
+ payload: dict,
25
+ read_timeout: int = DEFAULT_LAMBDA_READ_TIMEOUT,
26
+ ) -> dict | list | None:
27
+ client = _get_lambda_client(read_timeout)
28
+
29
+ response = client.invoke(
30
+ FunctionName=function_name,
31
+ InvocationType="RequestResponse",
32
+ Payload=json.dumps(payload),
33
+ )
34
+
35
+ return json.loads(response["Payload"].read().decode("utf-8"))
@@ -1,12 +1,18 @@
1
- import boto3
1
+ import json
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from typing import Optional
5
5
 
6
+ import boto3
7
+ from botocore.exceptions import ClientError
8
+
6
9
  from documente_shared.domain.entities.document import remove_slash_from_path
7
10
  from documente_shared.application.retry_utils import retry_on_size_integrity
8
11
 
9
12
 
13
+ MISSING_OBJECT_ERROR_CODES = ("NoSuchKey", "404")
14
+
15
+
10
16
  def remove_none_values(data: dict) -> dict: # noqa: WPS110
11
17
  return {key: value for key, value in data.items() if value is not None} # noqa: WPS110
12
18
 
@@ -39,6 +45,18 @@ class S3Bucket(object):
39
45
  return None
40
46
  return file_context["Body"] if "Body" in file_context else None
41
47
 
48
+ def get_json(self, file_key: str) -> Optional[dict]:
49
+ # Sin el retry de get(): aca la ausencia del objeto es una respuesta valida y no algo que
50
+ # convenga reintentar, asi que se puede usar para preguntar si un resultado ya esta.
51
+ cleaned_file_key = remove_slash_from_path(file_key)
52
+ try:
53
+ file_context = self._resource.Object(self.bucket_name, cleaned_file_key).get()
54
+ except ClientError as client_error:
55
+ if client_error.response["Error"]["Code"] in MISSING_OBJECT_ERROR_CODES:
56
+ return None
57
+ raise
58
+ return json.loads(file_context["Body"].read().decode("utf-8"))
59
+
42
60
  def upload(self, file_key: str, file_content, content_type: Optional[str] = None):
43
61
  cleaned_file_key = remove_slash_from_path(file_key)
44
62
  optional_params = {"ContentType": content_type}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "documente_shared"
3
- version = "0.1.202"
3
+ version = "0.1.203"
4
4
  description = "Shared utilities for LlamitAI projects"
5
5
  authors = [{ name = "Tech", email = "tech@llamitai.com" }]
6
6
  license = { text = "MIT" }
@@ -154,7 +154,7 @@ wheels = [
154
154
 
155
155
  [[package]]
156
156
  name = "documente-shared"
157
- version = "0.1.202"
157
+ version = "0.1.203"
158
158
  source = { editable = "." }
159
159
  dependencies = [
160
160
  { name = "boto3" },
@@ -200,7 +200,7 @@ name = "exceptiongroup"
200
200
  version = "1.3.1"
201
201
  source = { registry = "https://pypi.org/simple" }
202
202
  dependencies = [
203
- { name = "typing-extensions" },
203
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
204
204
  ]
205
205
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
206
206
  wheels = [
@@ -263,17 +263,17 @@ resolution-markers = [
263
263
  "python_full_version < '3.11'",
264
264
  ]
265
265
  dependencies = [
266
- { name = "colorama", marker = "sys_platform == 'win32'" },
267
- { name = "decorator" },
268
- { name = "exceptiongroup" },
269
- { name = "jedi" },
270
- { name = "matplotlib-inline" },
271
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
272
- { name = "prompt-toolkit" },
273
- { name = "pygments" },
274
- { name = "stack-data" },
275
- { name = "traitlets" },
276
- { name = "typing-extensions" },
266
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
267
+ { name = "decorator", marker = "python_full_version < '3.11'" },
268
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
269
+ { name = "jedi", marker = "python_full_version < '3.11'" },
270
+ { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
271
+ { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
272
+ { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
273
+ { name = "pygments", marker = "python_full_version < '3.11'" },
274
+ { name = "stack-data", marker = "python_full_version < '3.11'" },
275
+ { name = "traitlets", marker = "python_full_version < '3.11'" },
276
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
277
277
  ]
278
278
  sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" }
279
279
  wheels = [
@@ -288,17 +288,17 @@ resolution-markers = [
288
288
  "python_full_version >= '3.11'",
289
289
  ]
290
290
  dependencies = [
291
- { name = "colorama", marker = "sys_platform == 'win32'" },
292
- { name = "decorator" },
293
- { name = "ipython-pygments-lexers" },
294
- { name = "jedi" },
295
- { name = "matplotlib-inline" },
296
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
297
- { name = "prompt-toolkit" },
298
- { name = "pygments" },
299
- { name = "stack-data" },
300
- { name = "traitlets" },
301
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
291
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
292
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
293
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
294
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
295
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
296
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
297
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
298
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
299
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
300
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
301
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
302
302
  ]
303
303
  sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
304
304
  wheels = [
@@ -310,7 +310,7 @@ name = "ipython-pygments-lexers"
310
310
  version = "1.1.1"
311
311
  source = { registry = "https://pypi.org/simple" }
312
312
  dependencies = [
313
- { name = "pygments" },
313
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
314
314
  ]
315
315
  sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
316
316
  wheels = [
@@ -1,15 +0,0 @@
1
- import json
2
- import boto3
3
- from botocore.config import Config
4
-
5
-
6
- def invoke_lambda(function_name: str, payload: dict, read_timeout: int = 60) -> dict | list | None:
7
- client = boto3.client("lambda", config=Config(read_timeout=read_timeout))
8
-
9
- response = client.invoke(
10
- FunctionName=function_name,
11
- InvocationType="RequestResponse",
12
- Payload=json.dumps(payload),
13
- )
14
-
15
- return json.loads(response["Payload"].read().decode("utf-8"))