kinto 23.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kinto/__init__.py +92 -0
- kinto/__main__.py +249 -0
- kinto/authorization.py +134 -0
- kinto/config/__init__.py +94 -0
- kinto/config/kinto.tpl +270 -0
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +246 -0
- kinto/core/authentication.py +48 -0
- kinto/core/authorization.py +311 -0
- kinto/core/cache/__init__.py +131 -0
- kinto/core/cache/memcached.py +112 -0
- kinto/core/cache/memory.py +104 -0
- kinto/core/cache/postgresql/__init__.py +178 -0
- kinto/core/cache/postgresql/schema.sql +23 -0
- kinto/core/cache/testing.py +208 -0
- kinto/core/cornice/__init__.py +93 -0
- kinto/core/cornice/cors.py +144 -0
- kinto/core/cornice/errors.py +40 -0
- kinto/core/cornice/pyramidhook.py +373 -0
- kinto/core/cornice/renderer.py +89 -0
- kinto/core/cornice/resource.py +205 -0
- kinto/core/cornice/service.py +641 -0
- kinto/core/cornice/util.py +138 -0
- kinto/core/cornice/validators/__init__.py +94 -0
- kinto/core/cornice/validators/_colander.py +142 -0
- kinto/core/cornice/validators/_marshmallow.py +182 -0
- kinto/core/cornice_swagger/__init__.py +92 -0
- kinto/core/cornice_swagger/converters/__init__.py +21 -0
- kinto/core/cornice_swagger/converters/exceptions.py +6 -0
- kinto/core/cornice_swagger/converters/parameters.py +90 -0
- kinto/core/cornice_swagger/converters/schema.py +249 -0
- kinto/core/cornice_swagger/swagger.py +725 -0
- kinto/core/cornice_swagger/templates/index.html +73 -0
- kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
- kinto/core/cornice_swagger/util.py +42 -0
- kinto/core/cornice_swagger/views.py +78 -0
- kinto/core/decorators.py +74 -0
- kinto/core/errors.py +216 -0
- kinto/core/events.py +301 -0
- kinto/core/initialization.py +738 -0
- kinto/core/listeners/__init__.py +9 -0
- kinto/core/metrics.py +94 -0
- kinto/core/openapi.py +115 -0
- kinto/core/permission/__init__.py +202 -0
- kinto/core/permission/memory.py +167 -0
- kinto/core/permission/postgresql/__init__.py +489 -0
- kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
- kinto/core/permission/postgresql/schema.sql +41 -0
- kinto/core/permission/testing.py +487 -0
- kinto/core/resource/__init__.py +1311 -0
- kinto/core/resource/model.py +412 -0
- kinto/core/resource/schema.py +502 -0
- kinto/core/resource/viewset.py +230 -0
- kinto/core/schema.py +119 -0
- kinto/core/scripts.py +50 -0
- kinto/core/statsd.py +1 -0
- kinto/core/storage/__init__.py +436 -0
- kinto/core/storage/exceptions.py +53 -0
- kinto/core/storage/generators.py +58 -0
- kinto/core/storage/memory.py +651 -0
- kinto/core/storage/postgresql/__init__.py +1131 -0
- kinto/core/storage/postgresql/client.py +120 -0
- kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
- kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
- kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
- kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
- kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
- kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
- kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
- kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
- kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
- kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
- kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
- kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
- kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
- kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
- kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
- kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
- kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
- kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
- kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
- kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
- kinto/core/storage/postgresql/migrator.py +98 -0
- kinto/core/storage/postgresql/pool.py +55 -0
- kinto/core/storage/postgresql/schema.sql +143 -0
- kinto/core/storage/testing.py +1857 -0
- kinto/core/storage/utils.py +37 -0
- kinto/core/testing.py +182 -0
- kinto/core/utils.py +553 -0
- kinto/core/views/__init__.py +0 -0
- kinto/core/views/batch.py +163 -0
- kinto/core/views/errors.py +145 -0
- kinto/core/views/heartbeat.py +106 -0
- kinto/core/views/hello.py +69 -0
- kinto/core/views/openapi.py +35 -0
- kinto/core/views/version.py +50 -0
- kinto/events.py +3 -0
- kinto/plugins/__init__.py +0 -0
- kinto/plugins/accounts/__init__.py +94 -0
- kinto/plugins/accounts/authentication.py +63 -0
- kinto/plugins/accounts/scripts.py +61 -0
- kinto/plugins/accounts/utils.py +13 -0
- kinto/plugins/accounts/views.py +136 -0
- kinto/plugins/admin/README.md +3 -0
- kinto/plugins/admin/VERSION +1 -0
- kinto/plugins/admin/__init__.py +40 -0
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
- kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/admin/public/help.html +25 -0
- kinto/plugins/admin/views.py +42 -0
- kinto/plugins/default_bucket/__init__.py +191 -0
- kinto/plugins/flush.py +28 -0
- kinto/plugins/history/__init__.py +65 -0
- kinto/plugins/history/listener.py +181 -0
- kinto/plugins/history/views.py +66 -0
- kinto/plugins/openid/__init__.py +131 -0
- kinto/plugins/openid/utils.py +14 -0
- kinto/plugins/openid/views.py +193 -0
- kinto/plugins/prometheus.py +300 -0
- kinto/plugins/statsd.py +85 -0
- kinto/schema_validation.py +135 -0
- kinto/views/__init__.py +34 -0
- kinto/views/admin.py +195 -0
- kinto/views/buckets.py +45 -0
- kinto/views/collections.py +58 -0
- kinto/views/contribute.py +39 -0
- kinto/views/groups.py +90 -0
- kinto/views/permissions.py +235 -0
- kinto/views/records.py +133 -0
- kinto-23.2.1.dist-info/METADATA +232 -0
- kinto-23.2.1.dist-info/RECORD +142 -0
- kinto-23.2.1.dist-info/WHEEL +5 -0
- kinto-23.2.1.dist-info/entry_points.txt +5 -0
- kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
- kinto-23.2.1.dist-info/top_level.txt +1 -0
kinto/core/events.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import warnings
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import pyramid.tweens
|
|
7
|
+
import transaction
|
|
8
|
+
from pyramid.events import NewRequest
|
|
9
|
+
|
|
10
|
+
from kinto.core.utils import strip_uri_prefix
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ACTIONS(Enum):
|
|
17
|
+
CREATE = "create"
|
|
18
|
+
DELETE = "delete"
|
|
19
|
+
READ = "read"
|
|
20
|
+
UPDATE = "update"
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def from_string_list(elements):
|
|
24
|
+
return tuple(ACTIONS(el) for el in elements)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ResourceEvent:
|
|
28
|
+
def __init__(self, payload, request):
|
|
29
|
+
self.payload = payload
|
|
30
|
+
self.request = request
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return f"<{self.__class__.__name__} action={self.payload['action']} uri={self.payload['uri']}>"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def read_records(self):
|
|
37
|
+
message = "`read_records` is deprecated, use `read_objects` instead."
|
|
38
|
+
warnings.warn(message, DeprecationWarning)
|
|
39
|
+
return self.read_objects
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def impacted_records(self):
|
|
43
|
+
message = "`impacted_records` is deprecated, use `impacted_objects` instead."
|
|
44
|
+
warnings.warn(message, DeprecationWarning)
|
|
45
|
+
return self.impacted_objects
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ResourceRead(_ResourceEvent):
|
|
49
|
+
"""Triggered when a resource is being read."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, payload, read_objects, request):
|
|
52
|
+
super().__init__(payload, request)
|
|
53
|
+
self.read_objects = read_objects
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ResourceChanged(_ResourceEvent):
|
|
57
|
+
"""Triggered when a resource is being changed."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, payload, impacted_objects, request):
|
|
60
|
+
super().__init__(payload, request)
|
|
61
|
+
self.impacted_objects = impacted_objects
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AfterResourceRead(_ResourceEvent):
|
|
65
|
+
"""Triggered after a resource was successfully read."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, payload, read_objects, request):
|
|
68
|
+
super().__init__(payload, request)
|
|
69
|
+
self.read_objects = read_objects
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AfterResourceChanged(_ResourceEvent):
|
|
73
|
+
"""Triggered after a resource was successfully changed."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, payload, impacted_objects, request):
|
|
76
|
+
super().__init__(payload, request)
|
|
77
|
+
self.impacted_objects = impacted_objects
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EventCollector(object):
|
|
81
|
+
"""A collection to gather events emitted over the course of a request.
|
|
82
|
+
|
|
83
|
+
Events are gathered by parent id, resource type, and event
|
|
84
|
+
type. This serves as a primitive normalization so that we can emit
|
|
85
|
+
fewer events.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, cascade_level=1):
|
|
89
|
+
self.cascade_level = cascade_level
|
|
90
|
+
"""Current level of event cascade. When we start consuming the
|
|
91
|
+
gathered events, we increment it. This way, events emitted from
|
|
92
|
+
events listeners (cascade) are not merged with upstream ones.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
self.event_dict = OrderedDict()
|
|
96
|
+
"""The events as collected so far.
|
|
97
|
+
|
|
98
|
+
The key of the event_dict is a quadruple (cascade_level, resource_name,
|
|
99
|
+
parent_id, action). The value is a triple (impacted, request,
|
|
100
|
+
payload). If the same (cascade_level, resource_name, parent_id, action) is
|
|
101
|
+
encountered, we just extend the existing impacted with the new
|
|
102
|
+
impacted. N.B. this means all values in the payload must not
|
|
103
|
+
be specific to a single impacted_object. See
|
|
104
|
+
https://github.com/Kinto/kinto/issues/945 and
|
|
105
|
+
https://github.com/Kinto/kinto/issues/1731.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def add_event(self, resource_name, parent_id, action, payload, impacted, request):
|
|
109
|
+
key = (self.cascade_level, resource_name, parent_id, action)
|
|
110
|
+
if key not in self.event_dict:
|
|
111
|
+
value = (payload, impacted, request)
|
|
112
|
+
self.event_dict[key] = value
|
|
113
|
+
else:
|
|
114
|
+
old_value = self.event_dict[key]
|
|
115
|
+
(old_payload, old_impacted, old_request) = old_value
|
|
116
|
+
# May be a good idea to assert that old_payload == payload here.
|
|
117
|
+
self.event_dict[key] = (old_payload, old_impacted + impacted, old_request)
|
|
118
|
+
|
|
119
|
+
def drain(self):
|
|
120
|
+
"""Return an iterator that removes elements from this EventCollector.
|
|
121
|
+
|
|
122
|
+
This can be used to process events while still allowing events
|
|
123
|
+
to be added (for instance, as part of a cascade where events
|
|
124
|
+
add other events).
|
|
125
|
+
|
|
126
|
+
Items yielded will be of a tuple suitable for using as
|
|
127
|
+
arguments to EventCollector.add_event.
|
|
128
|
+
"""
|
|
129
|
+
# Since we start consuming the gathered events, we increment the cascade level.
|
|
130
|
+
self.cascade_level += 1
|
|
131
|
+
return EventCollectorDrain(self)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class EventCollectorDrain(object):
|
|
135
|
+
"""An iterator that drains an EventCollector.
|
|
136
|
+
|
|
137
|
+
Get one using EventCollector.drain()."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, event_collector):
|
|
140
|
+
self.event_collector = event_collector
|
|
141
|
+
|
|
142
|
+
def __iter__(self):
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def __next__(self):
|
|
146
|
+
if self.event_collector.event_dict:
|
|
147
|
+
# Get the "first" key in insertion order, so as to process
|
|
148
|
+
# events in the same order they were queued.
|
|
149
|
+
key = next(iter(self.event_collector.event_dict.keys()))
|
|
150
|
+
value = self.event_collector.event_dict.pop(key)
|
|
151
|
+
return key + value
|
|
152
|
+
else:
|
|
153
|
+
raise StopIteration
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def notify_resource_events_before(handler, registry):
|
|
157
|
+
"""Tween that runs ResourceChanged events.
|
|
158
|
+
|
|
159
|
+
This tween runs under EXCVIEW, so exceptions raised by
|
|
160
|
+
ResourceChanged events are caught and dealt the same as any other
|
|
161
|
+
exceptions.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def tween(request):
|
|
166
|
+
response = handler(request)
|
|
167
|
+
for event in request.get_resource_events():
|
|
168
|
+
request.registry.notify(event)
|
|
169
|
+
|
|
170
|
+
return response
|
|
171
|
+
|
|
172
|
+
return tween
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def setup_transaction_hook(config):
|
|
176
|
+
"""
|
|
177
|
+
Resource events are plugged with the transactions of ``pyramid_tm``.
|
|
178
|
+
|
|
179
|
+
Once a transaction is committed, ``AfterResourceRead`` and
|
|
180
|
+
``AfterResourceChanged`` events are sent.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def _notify_resource_events_after(success, request):
|
|
184
|
+
"""Notify the accumulated resource events if transaction succeeds."""
|
|
185
|
+
if not success: # pragma: no cover
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
for event in request.get_resource_events(after_commit=True):
|
|
189
|
+
try:
|
|
190
|
+
request.registry.notify(event)
|
|
191
|
+
except Exception:
|
|
192
|
+
logger.error("Unable to notify", exc_info=True)
|
|
193
|
+
|
|
194
|
+
def on_new_request(event):
|
|
195
|
+
"""When a new request comes in, hook on transaction commit."""
|
|
196
|
+
# Since there is one transaction per batch, ignore subrequests.
|
|
197
|
+
if hasattr(event.request, "parent"):
|
|
198
|
+
return
|
|
199
|
+
current = transaction.get()
|
|
200
|
+
current.addAfterCommitHook(_notify_resource_events_after, args=(event.request,))
|
|
201
|
+
|
|
202
|
+
config.add_subscriber(on_new_request, NewRequest)
|
|
203
|
+
config.add_tween(
|
|
204
|
+
"kinto.core.events.notify_resource_events_before", under=pyramid.tweens.EXCVIEW
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_resource_events(request, after_commit=False):
|
|
209
|
+
"""Generator to iterate the list of events triggered on resources.
|
|
210
|
+
|
|
211
|
+
The list is sorted chronologically (see OrderedDict).
|
|
212
|
+
|
|
213
|
+
This drains the resource_events currently in the request, which
|
|
214
|
+
allows us to process new events as they are added by current
|
|
215
|
+
events. However, once the iteration is over, we merge all the
|
|
216
|
+
events we've emitted into a new resource_events, which we store on
|
|
217
|
+
the request so we can reprocess the same events in an after-commit
|
|
218
|
+
tween.
|
|
219
|
+
|
|
220
|
+
This generator must be completely consumed!
|
|
221
|
+
"""
|
|
222
|
+
by_resource = request.bound_data.get("resource_events", EventCollector())
|
|
223
|
+
afterwards = EventCollector()
|
|
224
|
+
|
|
225
|
+
for event in by_resource.drain():
|
|
226
|
+
(_, resource_name, parent_id, action, payload, impacted, request) = event
|
|
227
|
+
afterwards.add_event(resource_name, parent_id, action, payload, impacted, request)
|
|
228
|
+
|
|
229
|
+
if after_commit:
|
|
230
|
+
if action == ACTIONS.READ:
|
|
231
|
+
event_cls = AfterResourceRead
|
|
232
|
+
else:
|
|
233
|
+
event_cls = AfterResourceChanged
|
|
234
|
+
else:
|
|
235
|
+
if action == ACTIONS.READ:
|
|
236
|
+
event_cls = ResourceRead
|
|
237
|
+
else:
|
|
238
|
+
event_cls = ResourceChanged
|
|
239
|
+
|
|
240
|
+
yield event_cls(payload, impacted, request)
|
|
241
|
+
|
|
242
|
+
request.bound_data["resource_events"] = afterwards
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def notify_resource_event(
|
|
246
|
+
request, parent_id, timestamp, data, action, old=None, resource_name=None, resource_data=None
|
|
247
|
+
):
|
|
248
|
+
"""Request helper to stack a resource event.
|
|
249
|
+
|
|
250
|
+
If a similar event (same resource, same action) already occured during the
|
|
251
|
+
current transaction (e.g. batch) then just extend the impacted objects of
|
|
252
|
+
the previous one.
|
|
253
|
+
|
|
254
|
+
:param resource_name: The name of the resource on which the event
|
|
255
|
+
happened (taken from the request if not provided).
|
|
256
|
+
:param resource_data: Information about the resource on which the
|
|
257
|
+
event is being emitted. Usually contains information about how
|
|
258
|
+
to find this object in the hierarchy (for instance,
|
|
259
|
+
``bucket_id`` and ``collection_id`` for a record). Taken from
|
|
260
|
+
the request matchdict if absent.
|
|
261
|
+
:type resource_data: dict
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
if action == ACTIONS.READ:
|
|
265
|
+
if not isinstance(data, list):
|
|
266
|
+
data = [data]
|
|
267
|
+
impacted = data
|
|
268
|
+
elif action == ACTIONS.CREATE:
|
|
269
|
+
impacted = [{"new": data}]
|
|
270
|
+
elif action == ACTIONS.DELETE:
|
|
271
|
+
if not isinstance(data, list):
|
|
272
|
+
impacted = [{"new": data, "old": old}]
|
|
273
|
+
else:
|
|
274
|
+
impacted = []
|
|
275
|
+
for i, new in enumerate(data):
|
|
276
|
+
impacted.append({"new": new, "old": old[i]})
|
|
277
|
+
else: # ACTIONS.UPDATE:
|
|
278
|
+
impacted = [{"new": data, "old": old}]
|
|
279
|
+
|
|
280
|
+
# Get previously triggered events.
|
|
281
|
+
events = request.bound_data.setdefault("resource_events", EventCollector())
|
|
282
|
+
|
|
283
|
+
resource_name = resource_name or request.current_resource_name
|
|
284
|
+
matchdict = resource_data or dict(request.matchdict)
|
|
285
|
+
|
|
286
|
+
payload = {
|
|
287
|
+
"timestamp": timestamp,
|
|
288
|
+
"action": action.value,
|
|
289
|
+
# Deprecated: don't actually use URI (see #945).
|
|
290
|
+
"uri": strip_uri_prefix(request.path),
|
|
291
|
+
"user_id": request.prefixed_userid,
|
|
292
|
+
"resource_name": resource_name,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Deprecated: don't actually use `resource_name_id` either (see #945).
|
|
296
|
+
if "id" in request.matchdict:
|
|
297
|
+
matchdict[resource_name + "_id"] = matchdict.pop("id")
|
|
298
|
+
|
|
299
|
+
payload.update(**matchdict)
|
|
300
|
+
|
|
301
|
+
events.add_event(resource_name, parent_id, action, payload, impacted, request)
|