aws-python-helper 1.8.0__tar.gz → 1.9.0__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 (61) hide show
  1. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/PKG-INFO +1 -1
  2. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/__init__.py +2 -1
  3. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/context/state_validator.py +10 -2
  4. aws_python_helper-1.9.0/aws_python_helper/monitoring/__init__.py +3 -0
  5. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/monitoring/run.py +67 -19
  6. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sns/publisher.py +40 -18
  7. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper.egg-info/PKG-INFO +1 -1
  8. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/pyproject.toml +1 -1
  9. aws_python_helper-1.8.0/aws_python_helper/monitoring/__init__.py +0 -3
  10. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/README.md +0 -0
  11. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/__init__.py +0 -0
  12. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/auth_middleware.py +0 -0
  13. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/auth_validators.py +0 -0
  14. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/base.py +0 -0
  15. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/dispatcher.py +0 -0
  16. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/exceptions.py +0 -0
  17. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/fetcher.py +0 -0
  18. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/api/handler.py +0 -0
  19. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/context/__init__.py +0 -0
  20. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/context/session.py +0 -0
  21. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/database/__init__.py +0 -0
  22. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/database/database_proxy.py +0 -0
  23. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  24. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  25. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/database/mongo_manager.py +0 -0
  26. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/fargate/__init__.py +0 -0
  27. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/fargate/executor.py +0 -0
  28. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/fargate/fetcher.py +0 -0
  29. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/fargate/handler.py +0 -0
  30. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/fargate/task_base.py +0 -0
  31. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/invoker/__init__.py +0 -0
  32. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/invoker/api_client.py +0 -0
  33. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/invoker/exceptions.py +0 -0
  34. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
  35. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  36. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/lambda_standalone/base.py +0 -0
  37. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  38. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  39. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/model_index_sync/__init__.py +0 -0
  40. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/model_index_sync/base.py +0 -0
  41. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/model_query/__init__.py +0 -0
  42. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/model_query/base.py +0 -0
  43. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/permissions/__init__.py +0 -0
  44. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/permissions/catalog.py +0 -0
  45. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/repository/__init__.py +0 -0
  46. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/repository/audit.py +0 -0
  47. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/repository/base.py +0 -0
  48. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sns/__init__.py +0 -0
  49. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sqs/__init__.py +0 -0
  50. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sqs/consumer_base.py +0 -0
  51. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sqs/fetcher.py +0 -0
  52. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/sqs/handler.py +0 -0
  53. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/utils/__init__.py +0 -0
  54. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/utils/json_encoder.py +0 -0
  55. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/utils/response.py +0 -0
  56. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper/utils/serializer.py +0 -0
  57. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper.egg-info/SOURCES.txt +0 -0
  58. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  59. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper.egg-info/requires.txt +0 -0
  60. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  61. {aws_python_helper-1.8.0 → aws_python_helper-1.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: AWS Python Helper Framework
5
5
  Author-email: Fabian Claros <neufabiae@gmail.com>
6
6
  License-Expression: MIT
@@ -57,11 +57,12 @@ from .invoker import (
57
57
  )
58
58
 
59
59
 
60
- from .monitoring import process_run, RunReporter, ProcessRunPublisher
60
+ from .monitoring import process_run, resolve_code, RunReporter, ProcessRunPublisher
61
61
 
62
62
  __all__ = [
63
63
  'API',
64
64
  'process_run',
65
+ 'resolve_code',
65
66
  'RunReporter',
66
67
  'ProcessRunPublisher',
67
68
  'SQSConsumer',
@@ -11,6 +11,11 @@ class StateValidator:
11
11
  """
12
12
  Validates that a state exists and is active in the core.states collection.
13
13
 
14
+ A state counts as active with either `status: "active"` or `is_active: True`.
15
+ The boolean is what every service has written until now; the status is what
16
+ the region catalog replicates. Accepting both means the boolean can be
17
+ dropped once no service reads it, without a flag day.
18
+
14
19
  Uses an in-memory cache with a 5-minute TTL to avoid querying the database
15
20
  on every request. Only valid (active) states are cached — invalid states
16
21
  always hit the database so that recently activated states are picked up
@@ -23,7 +28,7 @@ class StateValidator:
23
28
  @classmethod
24
29
  async def validate(cls, state: str):
25
30
  """
26
- Validate that state exists in core.states with is_active=True.
31
+ Validate that state exists in core.states and is active.
27
32
 
28
33
  Args:
29
34
  state: The state name to validate
@@ -40,7 +45,10 @@ class StateValidator:
40
45
 
41
46
  from ..database.mongo_manager import MongoManager
42
47
  db = MongoManager.get_database('core')
43
- doc = await db.states.find_one({"name": state, "is_active": True})
48
+ doc = await db.states.find_one({
49
+ "name": state,
50
+ "$or": [{"status": "active"}, {"is_active": True}],
51
+ })
44
52
 
45
53
  if doc:
46
54
  cls._cache[state] = now
@@ -0,0 +1,3 @@
1
+ from .run import ProcessRunPublisher, RunReporter, process_run, resolve_code
2
+
3
+ __all__ = ['process_run', 'resolve_code', 'RunReporter', 'ProcessRunPublisher']
@@ -4,10 +4,21 @@ Two hand-written publishes would work, and the second one would get forgotten on
4
4
  the path that matters: the failure. So the boundary is a context manager — enter
5
5
  and it says it started, leave and it says how it went, exception included.
6
6
 
7
- Nothing here can raise into the caller. Monitoring must never be able to break
8
- the thing it monitors: a topic that does not exist, credentials that are wrong,
9
- SNS being slow are all reasons to lose a record, never reasons to fail a run that
10
- otherwise worked.
7
+ The process code is the name of the AWS resource doing the work, taken from the
8
+ environment. Nobody types it twice: a code invented here and typed again in the
9
+ panel is a code that will not match one day, and a mismatch is silent — the runs
10
+ arrive, core finds no process, and nothing is recorded. Taking it from the
11
+ environment means the panel can offer the real resource list and the two sides
12
+ cannot drift.
13
+
14
+ Nothing here decides whether a run is kept. It publishes; core has the database
15
+ and filters. Wrapping something in `process_run` is not turning monitoring on —
16
+ registering the process in the panel is.
17
+
18
+ Nothing here can raise into the caller either. Monitoring must never be able to
19
+ break the thing it monitors: a topic that does not exist, credentials that are
20
+ wrong, SNS being slow are all reasons to lose a record, never reasons to fail a
21
+ run that otherwise worked.
11
22
  """
12
23
 
13
24
  import logging
@@ -17,6 +28,7 @@ from contextlib import asynccontextmanager
17
28
  from datetime import datetime, timezone
18
29
  from typing import Any, Dict, Optional
19
30
 
31
+ from ..context.session import get_session
20
32
  from ..sns.publisher import SNSPublisher
21
33
 
22
34
  logger = logging.getLogger(__name__)
@@ -28,6 +40,11 @@ FAILED = "failed"
28
40
 
29
41
  TOPIC_ENV = "PROCESS_RUNS_TOPIC_ARN"
30
42
 
43
+ # De dónde sale el código del proceso, en orden. `PROCESS_CODE` es para lo que no
44
+ # es una lambda —una tarea de Fargate lo recibe en su definición— y el nombre de
45
+ # la función es lo que AWS deja puesto solo.
46
+ CODE_ENVS = ("PROCESS_CODE", "AWS_LAMBDA_FUNCTION_NAME")
47
+
31
48
 
32
49
  class ProcessRunPublisher(SNSPublisher):
33
50
  pass
@@ -81,12 +98,25 @@ def _now() -> str:
81
98
  return datetime.now(timezone.utc).isoformat()
82
99
 
83
100
 
101
+ def resolve_code(explicit: Optional[str] = None) -> Optional[str]:
102
+ """El código del proceso: el nombre del recurso que está corriendo."""
103
+ if explicit:
104
+ return explicit
105
+ for name in CODE_ENVS:
106
+ value = (os.getenv(name) or "").strip()
107
+ if value:
108
+ return value
109
+ return None
110
+
111
+
84
112
  @asynccontextmanager
85
113
  async def process_run(
86
- process_code: str,
114
+ process_code: Optional[str] = None,
87
115
  *,
88
116
  run_id: Optional[str] = None,
117
+ parent_run_id: Optional[str] = None,
89
118
  scope: Optional[str] = None,
119
+ state: Optional[str] = None,
90
120
  resource_arn: Optional[str] = None,
91
121
  request_id: Optional[str] = None,
92
122
  ):
@@ -101,47 +131,65 @@ async def process_run(
101
131
  se está vigilando, y registrarlo solo llenaría la pantalla de ejecuciones que
102
132
  nadie puede configurar.
103
133
  """
104
- reporter = RunReporter(run_id or str(uuid.uuid4()), process_code)
134
+ code = resolve_code(process_code)
135
+ if not code:
136
+ # Sin código no hay a qué proceso pertenecer. Se sigue adelante sin
137
+ # reportar: el trabajo importa más que su registro.
138
+ logger.warning(
139
+ "No process code: pass one, or set %s. The run was not reported",
140
+ " or ".join(CODE_ENVS),
141
+ )
142
+ yield RunReporter("", "")
143
+ return
144
+
145
+ # El estado sale de la sesión: core identifica un proceso por código *y*
146
+ # estado, porque una misma lambda corre para varios con frecuencias
147
+ # distintas.
148
+ if state is None:
149
+ session = get_session()
150
+ state = session.state if session else None
151
+
152
+ reporter = RunReporter(run_id or str(uuid.uuid4()), code)
105
153
  started_at = _now()
154
+ common = {
155
+ "run_id": reporter.run_id,
156
+ "process_code": code,
157
+ "state": state,
158
+ "parent_run_id": parent_run_id,
159
+ "request_id": request_id,
160
+ }
106
161
 
107
162
  await _publish({
108
- "run_id": reporter.run_id,
109
- "process_code": process_code,
163
+ **common,
110
164
  "status": LAUNCHED,
111
165
  "started_at": started_at,
112
166
  "scope": scope,
113
- # Lo único que AWS deja en el ambiente. El ARN completo y el id de la
114
- # invocación viven en el `context` del handler, que acá no se tiene: quien
115
- # lo tenga los pasa por `resource_arn` y `request_id`.
116
- "resource_arn": resource_arn or os.getenv("AWS_LAMBDA_FUNCTION_NAME") or None,
117
- "request_id": request_id,
167
+ # El ARN completo y el id de la invocación viven en el `context` del
168
+ # handler, que acá no se tiene: quien lo tenga los pasa por parámetro.
169
+ "resource_arn": resource_arn,
118
170
  })
119
171
 
120
172
  try:
121
173
  yield reporter
122
174
  except Exception as error:
123
175
  await _publish({
124
- "run_id": reporter.run_id,
125
- "process_code": process_code,
176
+ **common,
126
177
  "status": FAILED,
127
178
  "started_at": started_at,
128
179
  "finished_at": _now(),
129
180
  "error": f"{type(error).__name__}: {error}"[:2000],
130
181
  "counters": reporter.counters,
131
182
  "scope": reporter.scope or scope,
132
- "request_id": request_id,
133
183
  })
134
184
  # La excepción sigue su camino: el monitoreo anota lo que pasó, no lo
135
185
  # decide. Tragarla acá convertiría una falla en un éxito silencioso.
136
186
  raise
137
187
  else:
138
188
  await _publish({
139
- "run_id": reporter.run_id,
140
- "process_code": process_code,
189
+ **common,
141
190
  "status": SUCCESS,
142
191
  "started_at": started_at,
143
192
  "finished_at": _now(),
144
193
  "counters": reporter.counters,
145
194
  "scope": reporter.scope or scope,
146
- "request_id": request_id,
147
195
  })
@@ -15,6 +15,11 @@ from abc import ABC
15
15
  from ..context.session import get_session
16
16
 
17
17
 
18
+ # Lo que SNS acepta en una sola llamada a PublishBatch. No es configurable: es
19
+ # un límite del servicio, y pasarse falla el lote entero sin publicar nada.
20
+ MAX_BATCH_ENTRIES = 10
21
+
22
+
18
23
  class SNSPublisher(ABC):
19
24
  """
20
25
  Base class to publish messages to SNS
@@ -172,35 +177,52 @@ class SNSPublisher(ABC):
172
177
  message_ids_success = []
173
178
  message_ids_failed = []
174
179
 
175
- params = {
176
- 'TopicArn': self.topic_arn
177
- }
178
-
179
180
  for i, message in enumerate(messages):
180
181
  try:
181
182
  message_to_publish.append(self._format_message(message, i))
182
183
  except Exception as e:
183
184
  self.logger.error(f"Error publishing message {i} in batch: {e}")
185
+ message_ids_failed.append(None)
184
186
  # Continue with the other messages
185
187
  continue
186
188
 
187
- params['PublishBatchRequestEntries'] = message_to_publish
189
+ # De a diez, que es lo que SNS acepta por llamada. Mandar más falla el
190
+ # lote entero sin publicar nada, así que una lista larga no se pierde a
191
+ # medias: se pierde completa.
192
+ for start in range(0, len(message_to_publish), MAX_BATCH_ENTRIES):
193
+ chunk = message_to_publish[start:start + MAX_BATCH_ENTRIES]
188
194
 
189
- try:
190
- result = self.sns_client.publish_batch(**params)
191
- except Exception as e:
192
- self.logger.error(f"Error calling publish_batch to {self.topic_arn}: {e}")
193
- raise
195
+ # El Id sólo tiene que ser único dentro de su propia llamada.
196
+ entries = [
197
+ {**entry, 'Id': str(position)}
198
+ for position, entry in enumerate(chunk)
199
+ ]
200
+
201
+ try:
202
+ result = self.sns_client.publish_batch(
203
+ TopicArn=self.topic_arn,
204
+ PublishBatchRequestEntries=entries,
205
+ )
206
+ except Exception as e:
207
+ # Se sigue con los demás lotes: cortar acá dejaría sin publicar
208
+ # todo lo que venía detrás por un problema que puede ser de este
209
+ # lote solo.
210
+ self.logger.error(
211
+ f"Error calling publish_batch to {self.topic_arn} "
212
+ f"({start}-{start + len(chunk) - 1} of {len(message_to_publish)}): {e}"
213
+ )
214
+ message_ids_failed.extend([None] * len(chunk))
215
+ continue
194
216
 
195
- for message in result.get('Successful', []):
196
- message_ids_success.append(message['MessageId'])
217
+ for message in result.get('Successful', []):
218
+ message_ids_success.append(message['MessageId'])
197
219
 
198
- for message in result.get('Failed', []):
199
- self.logger.error(
200
- f"Failed to publish message {message.get('Id')} to {self.topic_arn}: "
201
- f"Code={message.get('Code')}, Message={message.get('Message')}"
202
- )
203
- message_ids_failed.append(None)
220
+ for message in result.get('Failed', []):
221
+ self.logger.error(
222
+ f"Failed to publish message {message.get('Id')} to {self.topic_arn}: "
223
+ f"Code={message.get('Code')}, Message={message.get('Message')}"
224
+ )
225
+ message_ids_failed.append(None)
204
226
 
205
227
  self.logger.info(
206
228
  f"Batch publish to {self.topic_arn}: "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: AWS Python Helper Framework
5
5
  Author-email: Fabian Claros <neufabiae@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aws-python-helper"
7
- version = "1.8.0"
7
+ version = "1.9.0"
8
8
  description = "AWS Python Helper Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +0,0 @@
1
- from .run import ProcessRunPublisher, RunReporter, process_run
2
-
3
- __all__ = ['process_run', 'RunReporter', 'ProcessRunPublisher']