aws-python-helper 1.4.0__tar.gz → 1.6.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.
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/PKG-INFO +1 -1
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/__init__.py +14 -0
- aws_python_helper-1.6.0/aws_python_helper/repository/__init__.py +19 -0
- aws_python_helper-1.6.0/aws_python_helper/repository/audit.py +261 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/repository/base.py +62 -1
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/PKG-INFO +1 -1
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/SOURCES.txt +1 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/pyproject.toml +1 -1
- aws_python_helper-1.4.0/aws_python_helper/repository/__init__.py +0 -3
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/README.md +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/dispatcher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/invoker/api_client.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_index_sync/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_index_sync/base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_query/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_query/base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/permissions/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/permissions/catalog.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/setup.cfg +0 -0
|
@@ -26,6 +26,14 @@ from .utils.serializer import serialize_mongo_types
|
|
|
26
26
|
|
|
27
27
|
# Repository
|
|
28
28
|
from .repository.base import Repository
|
|
29
|
+
from .repository.audit import (
|
|
30
|
+
ACTIVE,
|
|
31
|
+
DEFAULT_STATUSES,
|
|
32
|
+
INACTIVE,
|
|
33
|
+
InvalidStatusError,
|
|
34
|
+
current_actor,
|
|
35
|
+
public_audit,
|
|
36
|
+
)
|
|
29
37
|
|
|
30
38
|
# Context
|
|
31
39
|
from .context.session import Session, get_session, set_session
|
|
@@ -59,6 +67,12 @@ __all__ = [
|
|
|
59
67
|
'FargateExecutor',
|
|
60
68
|
'Lambda',
|
|
61
69
|
'Repository',
|
|
70
|
+
'current_actor',
|
|
71
|
+
'public_audit',
|
|
72
|
+
'ACTIVE',
|
|
73
|
+
'INACTIVE',
|
|
74
|
+
'DEFAULT_STATUSES',
|
|
75
|
+
'InvalidStatusError',
|
|
62
76
|
'fargate_handler',
|
|
63
77
|
'api_handler',
|
|
64
78
|
'sqs_handler',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .base import Repository
|
|
2
|
+
from .audit import (
|
|
3
|
+
ACTIVE,
|
|
4
|
+
DEFAULT_STATUSES,
|
|
5
|
+
INACTIVE,
|
|
6
|
+
InvalidStatusError,
|
|
7
|
+
current_actor,
|
|
8
|
+
public_audit,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'Repository',
|
|
13
|
+
'ACTIVE',
|
|
14
|
+
'INACTIVE',
|
|
15
|
+
'DEFAULT_STATUSES',
|
|
16
|
+
'InvalidStatusError',
|
|
17
|
+
'current_actor',
|
|
18
|
+
'public_audit',
|
|
19
|
+
]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework-managed fields on every write: who, when, and what state.
|
|
3
|
+
|
|
4
|
+
Records answer "who last touched this, and when" without any repository having to
|
|
5
|
+
remember to fill it in. The four audit fields —created_at, created_by,
|
|
6
|
+
updated_at, updated_by— plus `status` are injected by wrapping the Motor
|
|
7
|
+
collection, so call sites keep using `self.collection.insert_one(...)` exactly as
|
|
8
|
+
before.
|
|
9
|
+
|
|
10
|
+
`status` is the lifecycle of a record, uniform across every entity so a listing
|
|
11
|
+
can show it without knowing what it is looking at. Two values by default, active
|
|
12
|
+
and inactive; a repository declaring `statuses` gets its own set. A value outside
|
|
13
|
+
that set is refused rather than stored: a status nobody expected reaches a panel
|
|
14
|
+
as an unknown label and a query as a silent mismatch.
|
|
15
|
+
|
|
16
|
+
Not every write comes from a person. A scraper, a queue consumer or a scheduled
|
|
17
|
+
task writes too, and the stamp says so: "nobody knows" and "the system did it"
|
|
18
|
+
are different answers.
|
|
19
|
+
|
|
20
|
+
Author is stored denormalised —id plus the name as it read at the time— so
|
|
21
|
+
reading a record never needs a second query, and renaming someone later does not
|
|
22
|
+
rewrite history.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Any, Dict, List, Optional, Union
|
|
28
|
+
|
|
29
|
+
from ..context.session import get_session
|
|
30
|
+
|
|
31
|
+
USER = 'user'
|
|
32
|
+
SYSTEM = 'system'
|
|
33
|
+
|
|
34
|
+
CREATED_AT = 'created_at'
|
|
35
|
+
CREATED_BY = 'created_by'
|
|
36
|
+
UPDATED_AT = 'updated_at'
|
|
37
|
+
UPDATED_BY = 'updated_by'
|
|
38
|
+
STATUS = 'status'
|
|
39
|
+
|
|
40
|
+
ACTIVE = 'active'
|
|
41
|
+
INACTIVE = 'inactive'
|
|
42
|
+
DEFAULT_STATUSES = [ACTIVE, INACTIVE]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _system_actor() -> Dict[str, str]:
|
|
46
|
+
"""The process doing the writing, when no person is behind it."""
|
|
47
|
+
service = os.getenv('SERVICE_CODE') or os.getenv('NAMESPACE') or 'constitution'
|
|
48
|
+
# Cada entrypoint deja su rastro: la lambda su nombre, el task el suyo.
|
|
49
|
+
process = (
|
|
50
|
+
os.getenv('PROCESS_NAME')
|
|
51
|
+
or os.getenv('AWS_LAMBDA_FUNCTION_NAME')
|
|
52
|
+
or os.getenv('TASK_NAME')
|
|
53
|
+
or service
|
|
54
|
+
)
|
|
55
|
+
return {'type': SYSTEM, 'id': service, 'name': process}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def current_actor() -> Dict[str, str]:
|
|
59
|
+
"""Who is writing right now, according to the request-scoped session."""
|
|
60
|
+
try:
|
|
61
|
+
session = get_session()
|
|
62
|
+
except Exception:
|
|
63
|
+
return _system_actor()
|
|
64
|
+
|
|
65
|
+
user = getattr(session, 'user', None) if session else None
|
|
66
|
+
if not isinstance(user, dict):
|
|
67
|
+
return _system_actor()
|
|
68
|
+
|
|
69
|
+
user_id = user.get('_id') or user.get('id') or user.get('user_id')
|
|
70
|
+
if not user_id:
|
|
71
|
+
return _system_actor()
|
|
72
|
+
|
|
73
|
+
name = user.get('name') or user.get('email') or str(user_id)
|
|
74
|
+
return {'type': USER, 'id': str(user_id), 'name': name}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _now() -> datetime:
|
|
78
|
+
return datetime.now(timezone.utc)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InvalidStatusError(ValueError):
|
|
82
|
+
"""A status the repository does not declare."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_status(value: Any, statuses: Optional[List[str]]) -> None:
|
|
86
|
+
if statuses and value is not None and value not in statuses:
|
|
87
|
+
raise InvalidStatusError(
|
|
88
|
+
f"'{value}' is not a valid status. Declared: {', '.join(statuses)}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def stamp_new(
|
|
93
|
+
document: Dict[str, Any],
|
|
94
|
+
statuses: Optional[List[str]] = None,
|
|
95
|
+
default_status: Optional[str] = None,
|
|
96
|
+
) -> Dict[str, Any]:
|
|
97
|
+
"""The framework fields on a document being created.
|
|
98
|
+
|
|
99
|
+
Whatever the caller set explicitly wins: this fills gaps, it does not
|
|
100
|
+
overrule a repository that knows better. What it does not let pass is a
|
|
101
|
+
status outside the declared set.
|
|
102
|
+
"""
|
|
103
|
+
if not isinstance(document, dict):
|
|
104
|
+
return document
|
|
105
|
+
|
|
106
|
+
now, who = _now(), current_actor()
|
|
107
|
+
document.setdefault(CREATED_AT, now)
|
|
108
|
+
document.setdefault(UPDATED_AT, now)
|
|
109
|
+
document.setdefault(CREATED_BY, who)
|
|
110
|
+
document.setdefault(UPDATED_BY, who)
|
|
111
|
+
|
|
112
|
+
if statuses:
|
|
113
|
+
document.setdefault(STATUS, default_status or statuses[0])
|
|
114
|
+
_check_status(document.get(STATUS), statuses)
|
|
115
|
+
return document
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _merge_into_set(update: Dict[str, Any], values: Dict[str, Any], operator: str) -> None:
|
|
119
|
+
section = update.get(operator)
|
|
120
|
+
if not isinstance(section, dict):
|
|
121
|
+
section = {}
|
|
122
|
+
update[operator] = section
|
|
123
|
+
for key, value in values.items():
|
|
124
|
+
section.setdefault(key, value)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def stamp_update(
|
|
128
|
+
update: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
129
|
+
upsert: bool = False,
|
|
130
|
+
statuses: Optional[List[str]] = None,
|
|
131
|
+
default_status: Optional[str] = None,
|
|
132
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
133
|
+
"""The two 'updated' fields on a modification, and the 'created' ones on an upsert.
|
|
134
|
+
|
|
135
|
+
An update can also be an aggregation pipeline, and a pipeline takes stages,
|
|
136
|
+
not operators: there the stamp goes as one more `$set` stage at the end.
|
|
137
|
+
"""
|
|
138
|
+
now, who = _now(), current_actor()
|
|
139
|
+
touched = {UPDATED_AT: now, UPDATED_BY: who}
|
|
140
|
+
|
|
141
|
+
if isinstance(update, list):
|
|
142
|
+
# En un pipeline `$set` es una etapa y pisa lo anterior a propósito: va al
|
|
143
|
+
# final para que el sello no lo tape una etapa posterior.
|
|
144
|
+
return update + [{'$set': {UPDATED_AT: now, UPDATED_BY: who}}]
|
|
145
|
+
|
|
146
|
+
if not isinstance(update, dict):
|
|
147
|
+
return update
|
|
148
|
+
|
|
149
|
+
# Un reemplazo entero —sin operadores— es un documento, no una modificación.
|
|
150
|
+
if update and not any(key.startswith('$') for key in update):
|
|
151
|
+
return stamp_new(dict(update), statuses, default_status)
|
|
152
|
+
|
|
153
|
+
update = dict(update)
|
|
154
|
+
if statuses:
|
|
155
|
+
for operator in ('$set', '$setOnInsert'):
|
|
156
|
+
section = update.get(operator)
|
|
157
|
+
if isinstance(section, dict) and STATUS in section:
|
|
158
|
+
_check_status(section[STATUS], statuses)
|
|
159
|
+
|
|
160
|
+
_merge_into_set(update, touched, '$set')
|
|
161
|
+
if upsert:
|
|
162
|
+
# Si termina insertando, el registro nace ahora: created_* y el estado
|
|
163
|
+
# inicial sólo en ese caso.
|
|
164
|
+
born = {CREATED_AT: now, CREATED_BY: who}
|
|
165
|
+
if statuses:
|
|
166
|
+
born[STATUS] = default_status or statuses[0]
|
|
167
|
+
_merge_into_set(update, born, '$setOnInsert')
|
|
168
|
+
return update
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AuditedCollection:
|
|
172
|
+
"""A Motor collection that fills in who wrote, when, and the record's status.
|
|
173
|
+
|
|
174
|
+
Only the write methods are intercepted; everything else —find, aggregate,
|
|
175
|
+
count_documents, create_index— passes straight through untouched.
|
|
176
|
+
|
|
177
|
+
`bulk_write` is deliberately left alone: its operations carry their own
|
|
178
|
+
filters and updates, and silently rewriting them would be harder to predict
|
|
179
|
+
than asking for the stamp explicitly.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
__slots__ = ('_collection', '_statuses', '_default_status')
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
collection: Any,
|
|
187
|
+
statuses: Optional[List[str]] = None,
|
|
188
|
+
default_status: Optional[str] = None,
|
|
189
|
+
):
|
|
190
|
+
object.__setattr__(self, '_collection', collection)
|
|
191
|
+
object.__setattr__(self, '_statuses', statuses)
|
|
192
|
+
object.__setattr__(self, '_default_status', default_status)
|
|
193
|
+
|
|
194
|
+
def _new(self, document: Dict[str, Any]) -> Dict[str, Any]:
|
|
195
|
+
return stamp_new(document, self._statuses, self._default_status)
|
|
196
|
+
|
|
197
|
+
def _update(self, update: Any, upsert: bool) -> Any:
|
|
198
|
+
return stamp_update(update, upsert, self._statuses, self._default_status)
|
|
199
|
+
|
|
200
|
+
def __getattr__(self, name: str) -> Any:
|
|
201
|
+
return getattr(self._collection, name)
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
return f'AuditedCollection({self._collection!r})'
|
|
205
|
+
|
|
206
|
+
async def insert_one(self, document: Dict[str, Any], *args: Any, **kwargs: Any):
|
|
207
|
+
return await self._collection.insert_one(self._new(document), *args, **kwargs)
|
|
208
|
+
|
|
209
|
+
async def insert_many(self, documents: Any, *args: Any, **kwargs: Any):
|
|
210
|
+
return await self._collection.insert_many(
|
|
211
|
+
[self._new(document) for document in documents], *args, **kwargs
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def update_one(self, filter: Any, update: Any, *args: Any, **kwargs: Any):
|
|
215
|
+
return await self._collection.update_one(
|
|
216
|
+
filter, self._update(update, kwargs.get('upsert', False)), *args, **kwargs
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def update_many(self, filter: Any, update: Any, *args: Any, **kwargs: Any):
|
|
220
|
+
return await self._collection.update_many(
|
|
221
|
+
filter, self._update(update, kwargs.get('upsert', False)), *args, **kwargs
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def find_one_and_update(self, filter: Any, update: Any, *args: Any, **kwargs: Any):
|
|
225
|
+
return await self._collection.find_one_and_update(
|
|
226
|
+
filter, self._update(update, kwargs.get('upsert', False)), *args, **kwargs
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def replace_one(self, filter: Any, replacement: Any, *args: Any, **kwargs: Any):
|
|
230
|
+
return await self._collection.replace_one(
|
|
231
|
+
filter, self._new(dict(replacement)), *args, **kwargs
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def find_one_and_replace(self, filter: Any, replacement: Any, *args: Any, **kwargs: Any):
|
|
235
|
+
return await self._collection.find_one_and_replace(
|
|
236
|
+
filter, self._new(dict(replacement)), *args, **kwargs
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def public_audit(document: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
241
|
+
"""Leaves the audit fields ready to show: name and type, dates in ISO.
|
|
242
|
+
|
|
243
|
+
The author travels as name plus type because a panel has to tell a person
|
|
244
|
+
from a process, not just print a string.
|
|
245
|
+
"""
|
|
246
|
+
if not isinstance(document, dict):
|
|
247
|
+
return document
|
|
248
|
+
|
|
249
|
+
document = dict(document)
|
|
250
|
+
for field in (CREATED_BY, UPDATED_BY):
|
|
251
|
+
value = document.get(field)
|
|
252
|
+
if isinstance(value, dict):
|
|
253
|
+
document[field] = {'name': value.get('name'), 'type': value.get('type', USER)}
|
|
254
|
+
for field in (CREATED_AT, UPDATED_AT):
|
|
255
|
+
value = document.get(field)
|
|
256
|
+
if isinstance(value, datetime):
|
|
257
|
+
# Sin tzinfo Mongo devuelve naive, y el navegador lo leería como hora local.
|
|
258
|
+
if value.tzinfo is None:
|
|
259
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
260
|
+
document[field] = value.isoformat()
|
|
261
|
+
return document
|
|
@@ -13,6 +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
17
|
|
|
17
18
|
|
|
18
19
|
class Repository(ABC):
|
|
@@ -35,6 +36,13 @@ class Repository(ABC):
|
|
|
35
36
|
is_external (bool): Whether to use an external MongoDB cluster. Default: False.
|
|
36
37
|
cluster_name (str): External cluster name. Required if is_external=True.
|
|
37
38
|
indexes (list): List of index definitions to create automatically.
|
|
39
|
+
audit (bool): Stamp created_at/created_by/updated_at/updated_by on every
|
|
40
|
+
write. Default: True — no repository has to remember to do it.
|
|
41
|
+
statuses (list[str] | None): Allowed values of the record's `status`.
|
|
42
|
+
Default: ['active', 'inactive']. None means this collection has no
|
|
43
|
+
status at all.
|
|
44
|
+
default_status (str): What a new record gets. Default: the first of
|
|
45
|
+
`statuses`.
|
|
38
46
|
|
|
39
47
|
Usage:
|
|
40
48
|
# Core repository — always uses 'core' database
|
|
@@ -138,6 +146,53 @@ class Repository(ABC):
|
|
|
138
146
|
"""Whether this repository uses an external MongoDB cluster. Default: False."""
|
|
139
147
|
return False
|
|
140
148
|
|
|
149
|
+
@property
|
|
150
|
+
def statuses(self) -> Optional[List[str]]:
|
|
151
|
+
"""
|
|
152
|
+
The values this record's `status` can take.
|
|
153
|
+
|
|
154
|
+
Every entity carries one, and the same two by default, so a listing can
|
|
155
|
+
show the state of anything without knowing what it is looking at. A
|
|
156
|
+
repository with its own lifecycle declares it:
|
|
157
|
+
|
|
158
|
+
class UserRepository(Repository):
|
|
159
|
+
@property
|
|
160
|
+
def statuses(self):
|
|
161
|
+
return ["active", "pending", "inactive"]
|
|
162
|
+
|
|
163
|
+
A write with a value outside this set is refused, not stored: an
|
|
164
|
+
unexpected status reaches a panel as an unknown label and a query as a
|
|
165
|
+
silent mismatch.
|
|
166
|
+
|
|
167
|
+
None for collections where a lifecycle makes no sense — sessions, tokens,
|
|
168
|
+
counters.
|
|
169
|
+
"""
|
|
170
|
+
return list(DEFAULT_STATUSES)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def default_status(self) -> Optional[str]:
|
|
174
|
+
"""What a record gets when nobody says otherwise. First of `statuses`."""
|
|
175
|
+
declared = self.statuses
|
|
176
|
+
return declared[0] if declared else None
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def audit(self) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Whether writes carry created_at/created_by/updated_at/updated_by.
|
|
182
|
+
|
|
183
|
+
On by default, so a record always answers who last touched it and when
|
|
184
|
+
without any repository having to fill it in. The author is a person when
|
|
185
|
+
a request carries a token, and the process itself otherwise.
|
|
186
|
+
|
|
187
|
+
Turn it off only where the stamp would be noise: collections that already
|
|
188
|
+
are a log of who did what (sessions, tokens), or raw scraped data written
|
|
189
|
+
in bulk where the author is always the same process.
|
|
190
|
+
|
|
191
|
+
class TokenRepository(Repository):
|
|
192
|
+
audit = False
|
|
193
|
+
"""
|
|
194
|
+
return True
|
|
195
|
+
|
|
141
196
|
@property
|
|
142
197
|
def cluster_name(self) -> Optional[str]:
|
|
143
198
|
"""
|
|
@@ -198,7 +253,13 @@ class Repository(ABC):
|
|
|
198
253
|
else:
|
|
199
254
|
db = MongoManager.get_database(self.database_name)
|
|
200
255
|
|
|
201
|
-
|
|
256
|
+
collection = db[self.collection_name]
|
|
257
|
+
# La envoltura completa las escrituras; todo lo demás pasa de largo.
|
|
258
|
+
self._collection_cache[key] = (
|
|
259
|
+
AuditedCollection(collection, self.statuses, self.default_status)
|
|
260
|
+
if self.audit
|
|
261
|
+
else collection
|
|
262
|
+
)
|
|
202
263
|
|
|
203
264
|
return self._collection_cache[key]
|
|
204
265
|
|
|
@@ -42,6 +42,7 @@ aws_python_helper/model_query/base.py
|
|
|
42
42
|
aws_python_helper/permissions/__init__.py
|
|
43
43
|
aws_python_helper/permissions/catalog.py
|
|
44
44
|
aws_python_helper/repository/__init__.py
|
|
45
|
+
aws_python_helper/repository/audit.py
|
|
45
46
|
aws_python_helper/repository/base.py
|
|
46
47
|
aws_python_helper/sns/__init__.py
|
|
47
48
|
aws_python_helper/sns/publisher.py
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/auth_middleware.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/api/auth_validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/context/state_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/database_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/database/mongo_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/invoker/lambda_invoker.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_index_sync/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_index_sync/base.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/model_query/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/permissions/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper/permissions/catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.4.0 → aws_python_helper-1.6.0}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|