aws-python-helper 1.10.0__tar.gz → 1.12.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 (62) hide show
  1. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/PKG-INFO +1 -1
  2. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/invoker/api_client.py +60 -20
  3. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/repository/audit.py +20 -11
  4. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/repository/base.py +24 -2
  5. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper.egg-info/PKG-INFO +1 -1
  6. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/pyproject.toml +1 -1
  7. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/tests/test_audit_stamp.py +28 -1
  8. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/README.md +0 -0
  9. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/__init__.py +0 -0
  10. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/__init__.py +0 -0
  11. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/auth_middleware.py +0 -0
  12. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/auth_validators.py +0 -0
  13. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/base.py +0 -0
  14. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/dispatcher.py +0 -0
  15. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/exceptions.py +0 -0
  16. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/fetcher.py +0 -0
  17. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/api/handler.py +0 -0
  18. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/context/__init__.py +0 -0
  19. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/context/session.py +0 -0
  20. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/context/state_validator.py +0 -0
  21. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/database/__init__.py +0 -0
  22. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/database/database_proxy.py +0 -0
  23. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  24. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  25. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/database/mongo_manager.py +0 -0
  26. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/fargate/__init__.py +0 -0
  27. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/fargate/executor.py +0 -0
  28. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/fargate/fetcher.py +0 -0
  29. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/fargate/handler.py +0 -0
  30. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/fargate/task_base.py +0 -0
  31. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/invoker/__init__.py +0 -0
  32. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/invoker/exceptions.py +0 -0
  33. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
  34. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  35. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/lambda_standalone/base.py +0 -0
  36. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  37. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  38. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/model_index_sync/__init__.py +0 -0
  39. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/model_index_sync/base.py +0 -0
  40. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/model_query/__init__.py +0 -0
  41. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/model_query/base.py +0 -0
  42. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/monitoring/__init__.py +0 -0
  43. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/monitoring/run.py +0 -0
  44. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/permissions/__init__.py +0 -0
  45. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/permissions/catalog.py +0 -0
  46. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/repository/__init__.py +0 -0
  47. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sns/__init__.py +0 -0
  48. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sns/publisher.py +0 -0
  49. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sqs/__init__.py +0 -0
  50. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sqs/consumer_base.py +0 -0
  51. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sqs/fetcher.py +0 -0
  52. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/sqs/handler.py +0 -0
  53. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/utils/__init__.py +0 -0
  54. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/utils/json_encoder.py +0 -0
  55. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/utils/response.py +0 -0
  56. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper/utils/serializer.py +0 -0
  57. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper.egg-info/SOURCES.txt +0 -0
  58. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  59. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper.egg-info/requires.txt +0 -0
  60. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  61. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/setup.cfg +0 -0
  62. {aws_python_helper-1.10.0 → aws_python_helper-1.12.0}/tests/test_sns_publisher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 1.10.0
3
+ Version: 1.12.0
4
4
  Summary: AWS Python Helper Framework
5
5
  Author-email: Fabian Claros <neufabiae@gmail.com>
6
6
  License-Expression: MIT
@@ -1,4 +1,3 @@
1
- import json
2
1
  import logging
3
2
  import os
4
3
  from typing import Any, Dict, Optional
@@ -13,26 +12,58 @@ _AUTH_HEADER = "Authorization"
13
12
  _BEARER_PREFIX = "Bearer"
14
13
 
15
14
 
16
- def _resolve_service_url(service_name: str) -> str:
17
- raw = os.getenv("MICROSERVICE_URLS", "")
18
- if not raw:
15
+ CORE = "core"
16
+
17
+ # Lo que este contenedor ya averiguó. Vive lo que vive la lambda, que es lo que
18
+ # se quiere: un servicio redesplegado con otra URL se conoce en el próximo
19
+ # arranque en frío, no dentro de horas.
20
+ _RESOLVED: Dict[str, str] = {}
21
+
22
+
23
+ def _core_url() -> str:
24
+ """Dónde está core. Es lo único que un servicio necesita saber de memoria.
25
+
26
+ Todo lo demás se lo pregunta a core, que lleva el registro de quién existe y
27
+ dónde responde. Una sola variable por servicio en vez de un mapa que había
28
+ que escribir en el secret de cada uno y mantener sincronizado a mano.
29
+ """
30
+ url = (os.getenv("CORE_API_URL") or "").rstrip("/")
31
+ if not url:
19
32
  raise ServiceNotConfiguredError(
20
- "MICROSERVICE_URLS environment variable is not set"
33
+ "CORE_API_URL is not set: this service cannot reach core, and core is "
34
+ "where it learns about everything else."
21
35
  )
22
- try:
23
- urls = json.loads(raw)
24
- except json.JSONDecodeError as exc:
25
- raise ServiceNotConfiguredError(
26
- "MICROSERVICE_URLS is not valid JSON"
27
- ) from exc
36
+ return url
28
37
 
29
- url = urls.get(service_name)
38
+
39
+ async def resolve_service_url(service_name: str) -> str:
40
+ """La dirección de un servicio, según el registro de core."""
41
+ if service_name == CORE:
42
+ return _core_url()
43
+
44
+ if service_name in _RESOLVED:
45
+ return _RESOLVED[service_name]
46
+
47
+ token = _resolve_token()
48
+ headers = {_AUTH_HEADER: f"{_BEARER_PREFIX} {token}"} if token else {}
49
+
50
+ async with httpx.AsyncClient(timeout=10) as client:
51
+ response = await client.get(f"{_core_url()}/services", headers=headers)
52
+ response.raise_for_status()
53
+ services = (response.json() or {}).get("services") or []
54
+
55
+ for service in services:
56
+ if service.get("api_url"):
57
+ _RESOLVED[service["code"]] = service["api_url"].rstrip("/")
58
+
59
+ url = _RESOLVED.get(service_name)
30
60
  if not url:
31
- available = ", ".join(urls.keys()) or "none"
61
+ known = ", ".join(sorted(_RESOLVED)) or "none"
32
62
  raise ServiceNotConfiguredError(
33
- f"Service '{service_name}' not found in MICROSERVICE_URLS. Available: {available}"
63
+ f"Core does not know a service called '{service_name}'. Registered: {known}. "
64
+ "A service appears here when it deploys."
34
65
  )
35
- return url.rstrip("/")
66
+ return url
36
67
 
37
68
 
38
69
  def _resolve_token() -> Optional[str]:
@@ -61,8 +92,8 @@ class ApiClient:
61
92
  client = ApiClient("dockets", headers={"constitution-state": "CT"})
62
93
 
63
94
  Environment variables:
64
- MICROSERVICE_URLS JSON map of service name → base URL
65
- e.g. {"dockets": "https://api.example.com/"}
95
+ CORE_API_URL Where core answers. The only address a service
96
+ holds; everything else it asks core for.
66
97
  INTER_SERVICE_TOKEN Bearer token for service-to-service calls
67
98
  AUTH_BYPASS_TOKEN Fallback if INTER_SERVICE_TOKEN is not set
68
99
  """
@@ -73,7 +104,8 @@ class ApiClient:
73
104
  headers: Optional[Dict[str, str]] = None,
74
105
  timeout: int = 30,
75
106
  ):
76
- self._base_url = _resolve_service_url(service_name)
107
+ self._service_name = service_name
108
+ self._base_url: Optional[str] = None
77
109
  self._timeout = timeout
78
110
  self._headers = self._build_default_headers(headers or {})
79
111
  self.logger = logging.getLogger(self.__class__.__name__)
@@ -86,7 +118,15 @@ class ApiClient:
86
118
  headers.update(extra)
87
119
  return headers
88
120
 
89
- def _url(self, path: str) -> str:
121
+ async def _url(self, path: str) -> str:
122
+ """La URL completa, resolviendo el servicio la primera vez que hace falta.
123
+
124
+ Se resuelve al usarse y no al construirse porque preguntarle a core es
125
+ una llamada de red, y un constructor que hace una llamada de red no se
126
+ puede usar en ningún lado sin pensarlo.
127
+ """
128
+ if self._base_url is None:
129
+ self._base_url = await resolve_service_url(self._service_name)
90
130
  return f"{self._base_url}/{path.lstrip('/')}"
91
131
 
92
132
  async def get(self, path: str, params: Optional[Dict] = None) -> Any:
@@ -120,7 +160,7 @@ class ApiClient:
120
160
  params: Optional[Dict] = None,
121
161
  body: Optional[Dict] = None,
122
162
  ) -> Any:
123
- url = self._url(path)
163
+ url = await self._url(path)
124
164
  try:
125
165
  async with httpx.AsyncClient(timeout=self._timeout) as client:
126
166
  response = await client.request(
@@ -82,10 +82,13 @@ class InvalidStatusError(ValueError):
82
82
  """A status the repository does not declare."""
83
83
 
84
84
 
85
- def _check_status(value: Any, statuses: Optional[List[str]]) -> None:
85
+ def _check_status(
86
+ value: Any, statuses: Optional[List[str]], field: str = STATUS
87
+ ) -> None:
86
88
  if statuses and value is not None and value not in statuses:
87
89
  raise InvalidStatusError(
88
- f"'{value}' is not a valid status. Declared: {', '.join(statuses)}"
90
+ f"'{value}' is not a valid value for '{field}'. "
91
+ f"Declared: {', '.join(statuses)}"
89
92
  )
90
93
 
91
94
 
@@ -93,6 +96,7 @@ def stamp_new(
93
96
  document: Dict[str, Any],
94
97
  statuses: Optional[List[str]] = None,
95
98
  default_status: Optional[str] = None,
99
+ status_field: str = STATUS,
96
100
  ) -> Dict[str, Any]:
97
101
  """The framework fields on a document being created.
98
102
 
@@ -110,8 +114,8 @@ def stamp_new(
110
114
  document.setdefault(UPDATED_BY, who)
111
115
 
112
116
  if statuses:
113
- document.setdefault(STATUS, default_status or statuses[0])
114
- _check_status(document.get(STATUS), statuses)
117
+ document.setdefault(status_field, default_status or statuses[0])
118
+ _check_status(document.get(status_field), statuses, status_field)
115
119
  return document
116
120
 
117
121
 
@@ -146,6 +150,7 @@ def stamp_update(
146
150
  upsert: bool = False,
147
151
  statuses: Optional[List[str]] = None,
148
152
  default_status: Optional[str] = None,
153
+ status_field: str = STATUS,
149
154
  ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
150
155
  """The two 'updated' fields on a modification, and the 'created' ones on an upsert.
151
156
 
@@ -165,14 +170,14 @@ def stamp_update(
165
170
 
166
171
  # Un reemplazo entero —sin operadores— es un documento, no una modificación.
167
172
  if update and not any(key.startswith('$') for key in update):
168
- return stamp_new(dict(update), statuses, default_status)
173
+ return stamp_new(dict(update), statuses, default_status, status_field)
169
174
 
170
175
  update = dict(update)
171
176
  if statuses:
172
177
  for operator in ('$set', '$setOnInsert'):
173
178
  section = update.get(operator)
174
- if isinstance(section, dict) and STATUS in section:
175
- _check_status(section[STATUS], statuses)
179
+ if isinstance(section, dict) and status_field in section:
180
+ _check_status(section[status_field], statuses, status_field)
176
181
 
177
182
  _merge_into_set(update, touched, '$set')
178
183
  if upsert:
@@ -180,7 +185,7 @@ def stamp_update(
180
185
  # inicial sólo en ese caso.
181
186
  born = {CREATED_AT: now, CREATED_BY: who}
182
187
  if statuses:
183
- born[STATUS] = default_status or statuses[0]
188
+ born[status_field] = default_status or statuses[0]
184
189
  born = {
185
190
  key: value
186
191
  for key, value in born.items()
@@ -201,23 +206,27 @@ class AuditedCollection:
201
206
  than asking for the stamp explicitly.
202
207
  """
203
208
 
204
- __slots__ = ('_collection', '_statuses', '_default_status')
209
+ __slots__ = ('_collection', '_statuses', '_default_status', '_status_field')
205
210
 
206
211
  def __init__(
207
212
  self,
208
213
  collection: Any,
209
214
  statuses: Optional[List[str]] = None,
210
215
  default_status: Optional[str] = None,
216
+ status_field: str = STATUS,
211
217
  ):
212
218
  object.__setattr__(self, '_collection', collection)
213
219
  object.__setattr__(self, '_statuses', statuses)
214
220
  object.__setattr__(self, '_default_status', default_status)
221
+ object.__setattr__(self, '_status_field', status_field)
215
222
 
216
223
  def _new(self, document: Dict[str, Any]) -> Dict[str, Any]:
217
- return stamp_new(document, self._statuses, self._default_status)
224
+ return stamp_new(document, self._statuses, self._default_status, self._status_field)
218
225
 
219
226
  def _update(self, update: Any, upsert: bool) -> Any:
220
- return stamp_update(update, upsert, self._statuses, self._default_status)
227
+ return stamp_update(
228
+ update, upsert, self._statuses, self._default_status, self._status_field
229
+ )
221
230
 
222
231
  def __getattr__(self, name: str) -> Any:
223
232
  return getattr(self._collection, name)
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Tuple
13
13
  from ..database.mongo_manager import MongoManager
14
14
  from ..database.external_mongo_manager import ExternalMongoManager
15
15
  from ..context.session import get_session
16
- from .audit import DEFAULT_STATUSES, AuditedCollection
16
+ from .audit import DEFAULT_STATUSES, STATUS, AuditedCollection
17
17
 
18
18
 
19
19
  class Repository(ABC):
@@ -169,6 +169,26 @@ class Repository(ABC):
169
169
  """
170
170
  return list(DEFAULT_STATUSES)
171
171
 
172
+ @property
173
+ def status_field(self) -> str:
174
+ """Cómo se llama el campo que lleva el ciclo de vida del registro.
175
+
176
+ `status` por defecto. Se cambia cuando una colección ya usa ese nombre
177
+ para otra cosa y migrarla no vale la pena: los 1,2 millones de empresas
178
+ traen el estado del registro del estado en `status`, y 5,7 millones de
179
+ parcels podrían traer otro. Renombrar en la base es lo más limpio cuando
180
+ se puede; cuando no, se le dice al framework dónde escribir el suyo.
181
+
182
+ class ParcelRepository(Repository):
183
+ @property
184
+ def status_field(self):
185
+ return "lifecycle_status"
186
+
187
+ Las dos cosas son independientes: `statuses` dice qué valores valen, esto
188
+ dice dónde se guardan.
189
+ """
190
+ return STATUS
191
+
172
192
  @property
173
193
  def default_status(self) -> Optional[str]:
174
194
  """What a record gets when nobody says otherwise. First of `statuses`."""
@@ -256,7 +276,9 @@ class Repository(ABC):
256
276
  collection = db[self.collection_name]
257
277
  # La envoltura completa las escrituras; todo lo demás pasa de largo.
258
278
  self._collection_cache[key] = (
259
- AuditedCollection(collection, self.statuses, self.default_status)
279
+ AuditedCollection(
280
+ collection, self.statuses, self.default_status, self.status_field
281
+ )
260
282
  if self.audit
261
283
  else collection
262
284
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 1.10.0
3
+ Version: 1.12.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.10.0"
7
+ version = "1.12.0"
8
8
  description = "AWS Python Helper Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -15,7 +15,7 @@ import sys
15
15
 
16
16
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
17
17
 
18
- from aws_python_helper.repository.audit import stamp_update # noqa: E402
18
+ from aws_python_helper.repository.audit import stamp_new, stamp_update # noqa: E402
19
19
 
20
20
  STATUSES = ["active", "inactive"]
21
21
 
@@ -82,6 +82,33 @@ result = stamp_update([{"$set": {"name": "ct"}}], upsert=True, statuses=STATUSES
82
82
  check("still a list", isinstance(result, list))
83
83
  check("stamp appended last", result[-1]["$set"].get("updated_at") is not None)
84
84
 
85
+ print("\nthe lifecycle field can be called something else")
86
+ result = stamp_new(
87
+ {"name": "x", "status": "FORFEITED"},
88
+ ["active", "inactive"],
89
+ "active",
90
+ status_field="lifecycle_status",
91
+ )
92
+ check("the collection's own status is untouched", result["status"] == "FORFEITED")
93
+ check("the framework writes its own field", result["lifecycle_status"] == "active")
94
+
95
+ result = stamp_update(
96
+ {"$set": {"name": "x"}},
97
+ upsert=True,
98
+ statuses=["active"],
99
+ default_status="active",
100
+ status_field="lifecycle_status",
101
+ )
102
+ check("and on an upsert it stamps that one", result["$setOnInsert"]["lifecycle_status"] == "active")
103
+ check("with no path conflict", not conflicts(result))
104
+
105
+ try:
106
+ stamp_new({"lifecycle_status": "made up"}, ["active"], status_field="lifecycle_status")
107
+ check("a value outside the vocabulary is refused", False)
108
+ except ValueError as error:
109
+ check("a value outside the vocabulary is refused", "lifecycle_status" in str(error),
110
+ "and the message names the field")
111
+
85
112
  if failures:
86
113
  print(f"\n{len(failures)} failed: {failures}")
87
114
  raise SystemExit(1)