backo 0.0.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.
backo/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ Module backo.
3
+ export all classes
4
+ """
5
+
6
+ from .item import Item
7
+ from .db_yml_connector import DBYmlConnector
8
+ from .db_mongo_connector import DBMongoConnector
9
+ from .current_user import current_user
10
+ from .error import Error, ErrorType
11
+ from .backoffice import Backoffice
12
+ from .collection import Collection
13
+ from .view import View
14
+ from .log import Logger, log_system
15
+ from .reference import Ref, RefsList, FillStrategy, DeleteStrategy
16
+ from .meta_data_handler import GenericMetaDataHandler, StandardMetaDataHandler
17
+ from .status import StatusType
18
+ from .action import Action
19
+ from .request_decorators import (
20
+ check_json,
21
+ check_method,
22
+ return_http_error,
23
+ error_to_http_handler,
24
+ )
25
+ from .api_toolbox import multidict_to_filter
backo/action.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ Module providing the action
3
+ """
4
+
5
+ # pylint: disable=wrong-import-position, no-member, import-error, protected-access, wrong-import-order, attribute-defined-outside-init
6
+
7
+
8
+ import sys
9
+
10
+ sys.path.insert(1, "../../stricto")
11
+ from stricto import Dict
12
+
13
+ from .log import log_system
14
+ from .error import Error, ErrorType
15
+ from .item import Item
16
+
17
+ log = log_system.get_or_create_logger("action")
18
+
19
+
20
+ class Action(Dict): # pylint: disable=too-many-instance-attributes
21
+ """
22
+ An action
23
+ """
24
+
25
+ def __init__(self, schema: dict, on_trig, **kwargs):
26
+ """
27
+ available arguments
28
+ """
29
+ self.backoffice = None
30
+ self.name = None
31
+ self.collection = None
32
+ self.on_trig = on_trig
33
+
34
+ # Add default right
35
+ if "can_execute" not in kwargs:
36
+ kwargs["can_execute"] = True
37
+ if "can_see" not in kwargs:
38
+ kwargs["can_see"] = True
39
+ kwargs["can_read"] = True
40
+ kwargs["can_modify"] = True
41
+ kwargs["exists"] = True
42
+
43
+ Dict.__init__(self, schema, **kwargs)
44
+
45
+ def check_params(self, param_name, o: Item) -> bool:
46
+ """
47
+ Check if can execute the action
48
+ """
49
+ p = self._params.get(param_name, False)
50
+ if not callable(p):
51
+ return bool(p)
52
+ return bool(p(self.backoffice, self.collection, self, o))
53
+
54
+ def can_see(self, o: Item) -> bool:
55
+ """
56
+ Check if this action exists for running
57
+ """
58
+ return self._rights.has_right("see", self, o)
59
+
60
+ def can_execute(self, o: Item) -> bool:
61
+ """
62
+ Check if can execute the action
63
+ object can be a Dict, a array of Dict, or None, depends ont the target for this actopn
64
+ """
65
+ return self._rights.has_right("execute", self, o)
66
+
67
+ def go(self, o: Item) -> None:
68
+ """
69
+ Launch the action
70
+
71
+ objeoct is the object (if exists)
72
+ """
73
+
74
+ if not self.can_see(o):
75
+ log.error("Try to launch non available action %r", self.name)
76
+ raise Error(
77
+ ErrorType.ACTION_NOT_AVAILABLE,
78
+ f"action {self.name} not available",
79
+ )
80
+
81
+ if not self.can_execute(o):
82
+ log.error("Try to execute forbidden action %r", self.name)
83
+ raise Error(
84
+ ErrorType.ACTION_FORBIDDEN,
85
+ f"action {self.name} forbidden",
86
+ )
87
+
88
+ log.debug("Execute action %r", self.name)
89
+ return self.on_trig(self, o)
backo/api_toolbox.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ The toolbox for api
3
+ A set of functions
4
+ """
5
+
6
+ import re
7
+ from werkzeug.datastructures import ImmutableMultiDict
8
+
9
+
10
+ def _append_to_filter(filter_as_dict: dict, key, value: list):
11
+ """
12
+ adding key to dict with transformation to int or float if we can
13
+ """
14
+
15
+ # Transform string to int or float
16
+ typed_value = []
17
+ for v in value:
18
+ try:
19
+ vv = int(v)
20
+ except ValueError:
21
+ try:
22
+ vv = float(v)
23
+ except ValueError:
24
+ vv = v
25
+ typed_value.append(vv)
26
+
27
+ val = typed_value[0] if len(typed_value) == 1 else typed_value
28
+
29
+ match = re.search(r"^([^\.]+)\.(.*)", key)
30
+ if not match:
31
+ filter_as_dict[key] = val
32
+ return
33
+
34
+ # a toto.$gt (with an operator)
35
+ if re.findall(r"^\$", match.group(2)):
36
+ filter_as_dict[match.group(1)] = (match.group(2), val)
37
+ return
38
+
39
+ sub = filter_as_dict.get(match.group(1), {})
40
+ if not isinstance(sub, dict):
41
+ sub = {}
42
+
43
+ _append_to_filter(sub, match.group(2), value)
44
+ filter_as_dict[match.group(1)] = sub
45
+
46
+
47
+ def multidict_to_filter(md: ImmutableMultiDict):
48
+ """
49
+ Transform a multi dict to filter (query string are immutable dict)
50
+
51
+ see match in stricto for definition of a filter
52
+ see https://tedboy.github.io/flask/generated/generated/werkzeug.ImmutableMultiDict.html
53
+
54
+
55
+ [ ('toto', 'miam'), ('titi.tutu', '23.2') ('tata.$gt', 11)] ->
56
+ {
57
+ 'toto' : "miam",
58
+ 'titi' : {
59
+ 'tutu' : 23.2
60
+ },
61
+ 'tata' : ( '$gt', 11 )
62
+ }
63
+ """
64
+
65
+ filter_as_dict = {}
66
+ for key in md.keys():
67
+
68
+ # ignoring keys starting with _
69
+ if re.match(r"^_.*", key):
70
+ continue
71
+
72
+ value = md.getlist(key)
73
+ _append_to_filter(filter_as_dict, key, value)
74
+
75
+ return filter_as_dict
backo/backoffice.py ADDED
@@ -0,0 +1,100 @@
1
+ """
2
+ The Backoffice module
3
+ """
4
+
5
+ import logging
6
+ from flask import Flask
7
+
8
+ from .item import Item
9
+ from .transaction import Transaction, OperatorType
10
+ from .collection import Collection
11
+ from .log import log_system
12
+
13
+ log = log_system.get_or_create_logger("backoffice", logging.DEBUG)
14
+
15
+
16
+ class Backoffice: # pylint: disable=too-many-instance-attributes
17
+ """
18
+ The main object, the aplication itself
19
+ """
20
+
21
+ def __init__(self, name: str):
22
+ """
23
+ initialize the backoffice with a name
24
+ """
25
+ self.name = name
26
+ self.collections = {}
27
+ self.transaction_id_reference = 1
28
+ self.transactions = {}
29
+
30
+ def register_collection(self, coll: Collection) -> None:
31
+ """
32
+ Register a collection into this backoffice
33
+ """
34
+ self.collections[coll.name] = coll
35
+ coll.backoffice = self
36
+ setattr(self, coll.name, coll)
37
+
38
+ def add_collection(self, coll: Collection) -> None:
39
+ """
40
+ Register a collection into the backoffice
41
+ """
42
+ return self.register_collection(coll)
43
+
44
+ def start_transaction(self) -> int:
45
+ """
46
+ Chose an Id and start the transaction structure
47
+ """
48
+ self.transaction_id_reference += 1
49
+ my_id = self.transaction_id_reference
50
+ self.transactions[my_id] = []
51
+ return my_id
52
+
53
+ def stop_transaction(self, transaction_id: int) -> None:
54
+ """
55
+ Close the transaction structure
56
+ """
57
+ del self.transactions[transaction_id]
58
+
59
+ def record_transaction(
60
+ self,
61
+ transaction_id: int,
62
+ collection: Collection,
63
+ operation: OperatorType,
64
+ _id: str,
65
+ obj: Item,
66
+ ) -> None:
67
+ """
68
+ Append an object to the transaction
69
+ """
70
+ if not transaction_id:
71
+ return
72
+ self.transactions[transaction_id].append(
73
+ Transaction(collection, operation, _id, obj)
74
+ )
75
+
76
+ def rollback_transaction(self, transaction_id: int) -> None:
77
+ """
78
+ An error occure, rollback objects
79
+ """
80
+ log.info(
81
+ "Rollback transactions %d with %d actions",
82
+ transaction_id,
83
+ len(self.transactions[transaction_id]),
84
+ )
85
+ while self.transactions[transaction_id]:
86
+ t = self.transactions[transaction_id].pop()
87
+ t.rollback(self)
88
+
89
+ del self.transactions[transaction_id]
90
+
91
+ def add_routes(self, flask_app: Flask, prefix: str = "") -> None:
92
+ """
93
+ Add all routes to flask application
94
+ """
95
+
96
+ my_path = f"/{prefix}/{self.name}/" if prefix else f"/{self.name}"
97
+ log.debug("Adding routes under %s", my_path)
98
+
99
+ for collection in self.collections.values():
100
+ collection.flask_add_routes(flask_app, my_path)
backo/collection.py ADDED
@@ -0,0 +1,415 @@
1
+ """
2
+ The Collection module
3
+ """
4
+
5
+ # pylint: disable=logging-fstring-interpolation
6
+ import json
7
+ import logging
8
+ import re
9
+ import sys
10
+
11
+ from flask import Flask, request, session
12
+
13
+ sys.path.insert(1, "../../stricto")
14
+ from stricto import StrictoEncoder, Rights
15
+
16
+
17
+ from .item import Item
18
+ from .action import Action
19
+ from .error import Error, ErrorType
20
+ from .log import log_system
21
+ from .request_decorators import error_to_http_handler
22
+ from .api_toolbox import multidict_to_filter
23
+ from .patch import Patch
24
+
25
+
26
+ log = log_system.get_or_create_logger("collection", logging.DEBUG)
27
+
28
+
29
+ class Collection:
30
+ """
31
+ The Collection refer to a "table"
32
+ """
33
+
34
+ def __init__(self, name, model: Item, db_handler, **kwargs):
35
+ """
36
+ available arguments
37
+ """
38
+ self.db_handler = db_handler
39
+ self.name = name
40
+ self.model = model.copy()
41
+ self.model.__dict__["_collection"] = self
42
+ self.model.set_db_handler(db_handler)
43
+
44
+ # Set rights
45
+ self._rights = Rights(read=True, create=True, delete=True, modify=True)
46
+ for key, right in kwargs.items():
47
+ a = re.findall(r"^can_(.*)$", key)
48
+ if a:
49
+ self._rights.add_or_modify_right(a[0], right)
50
+
51
+ # For filtering
52
+ self.refuse_filter = kwargs.pop("refuse_filter", None)
53
+
54
+ # For actions (aka some element work with datas)
55
+ self._actions = {}
56
+ self.backoffice = None
57
+
58
+ # For views
59
+ self._views = {}
60
+
61
+ def set(self, datas: dict | list) -> Item | list:
62
+ """
63
+ Set an object or a list of object
64
+ """
65
+ if isinstance(datas, dict):
66
+ o = self.new_item()
67
+ o.set(datas)
68
+ return o
69
+
70
+ if isinstance(datas, list):
71
+ l = []
72
+ for d in datas:
73
+ l.append(self.set(d))
74
+ return l
75
+
76
+ def define_view(self, name: str, list_of_selector: list) -> None:
77
+ """
78
+ add element into views
79
+ """
80
+ for selector in list_of_selector:
81
+ f = self.model.select(selector)
82
+ if f is None:
83
+ continue
84
+ if name not in f._views:
85
+ f._views.append(name)
86
+
87
+ def has_right(self, right_name: str, o: Item = None) -> bool:
88
+ """
89
+ Return the right for this collection
90
+ """
91
+ return self._rights.has_right(right_name, o)
92
+
93
+ def new_item(self):
94
+ """
95
+ return an Item
96
+ """
97
+ return self.model.copy()
98
+
99
+ def new(self):
100
+ """
101
+ return an Item
102
+ """
103
+ return self.new_item()
104
+
105
+ def create(self, obj: dict, **kwargs):
106
+ """
107
+ Create and save an item into the DB
108
+
109
+ transaction_id : The id of the transaction (used for rollback )
110
+ m_path : modification path, to avoid loop with references
111
+ """
112
+
113
+ if self._rights.has_right("create", None) is not True:
114
+ raise Error(
115
+ ErrorType.UNAUTHORIZED,
116
+ f"No rights to create in collection {self.name}.",
117
+ )
118
+
119
+ item = self.new_item()
120
+ item.create(obj, **kwargs)
121
+ return item
122
+
123
+ def get_other_collection(self, name):
124
+ """
125
+ Return another collection (ised by Ref and RefsList)
126
+ """
127
+ if self.backoffice is None:
128
+ raise Error(
129
+ ErrorType.COLLECTION_NOT_REGISTERED,
130
+ f"collection {self.name} not registered into an backoffice",
131
+ )
132
+ return self.backoffice.collections.get(name)
133
+
134
+ def register_action(self, name: str, action: Action):
135
+ """
136
+ add an action to this collection
137
+ this action will be related to an object
138
+ """
139
+ self._actions[name] = action
140
+ action.__dict__["backoffice"] = self.backoffice
141
+ action.__dict__["name"] = name
142
+ action.__dict__["collection"] = self
143
+
144
+ def add_action(self, name: str, action: Action):
145
+ """
146
+ add an action to this collection
147
+ this action will be related to an object
148
+ """
149
+ return self.register_action(name, action)
150
+
151
+ def drop(self):
152
+ """
153
+ Drop all elements
154
+ """
155
+ self.db_handler.drop()
156
+
157
+ def get_by_id(self, _id):
158
+ """
159
+ return an object by Id.
160
+ """
161
+
162
+ if self._rights.has_right("read", None) is not True:
163
+ raise Error(
164
+ ErrorType.UNAUTHORIZED,
165
+ f"No rights to create in collection {self.name}.",
166
+ )
167
+
168
+ obj = self.new_item()
169
+ obj.load(_id)
170
+ return obj
171
+
172
+ def select(
173
+ self,
174
+ db_filter,
175
+ match_filter=None,
176
+ view=None,
177
+ page_size=0,
178
+ num_of_element_to_skip=0,
179
+ db_sort_object={"_id": 1},
180
+ ):
181
+ """
182
+ Do a selection
183
+
184
+ db_filter : Is a filter related to the database system
185
+ match_filter : A filter for matching the object, independant
186
+ from the db. See match() in stricto
187
+ view : The view we want (See views in stricto)
188
+
189
+ """
190
+
191
+ if self._rights.has_right("read", None) is not True:
192
+ raise Error(
193
+ ErrorType.UNAUTHORIZED,
194
+ f"No rights to read the entire collection {self.name}.",
195
+ )
196
+
197
+ # Do the DB selection without pagination
198
+ db_list = self.db_handler.select(db_filter, {}, 0, 0, db_sort_object)
199
+ if not isinstance(db_list, list):
200
+ raise Error(
201
+ ErrorType.SELECT_ERROR,
202
+ f"select {self.name} error",
203
+ )
204
+ result = {
205
+ "result": [],
206
+ "total": 0,
207
+ "_view": view,
208
+ "_skip": num_of_element_to_skip,
209
+ "_page": page_size,
210
+ }
211
+
212
+ # Get the restriction filter
213
+ # rfilter = (
214
+ # self.refuse_filter() if callable(self.refuse_filter) else self.refuse_filter
215
+ # )
216
+
217
+ # Do the selection on the object
218
+ index = 0
219
+ log.debug(f"try match {match_filter} for {len(db_list)}")
220
+ for obj in db_list:
221
+ obj["_id"] = str(obj["_id"])
222
+ o = self.new_item()
223
+ o.set(obj)
224
+ o.set_status_saved()
225
+ # Do the post match filtering
226
+
227
+ # Ignore all elements matched by the refuse filter
228
+ if self._rights.has_right("read", o) is not True:
229
+ continue
230
+
231
+ if o.match(match_filter) is True:
232
+ if index >= num_of_element_to_skip:
233
+ if page_size == 0 or (
234
+ page_size > 0 and index < (num_of_element_to_skip + page_size)
235
+ ):
236
+ result["result"].append(o)
237
+ index += 1
238
+ else:
239
+ log.debug(f"Not matchs {match_filter} for {o}")
240
+
241
+ result["total"] = index
242
+ return result
243
+
244
+ def flask_add_routes(self, flask_app: Flask, my_path: str = "") -> None:
245
+ """
246
+ Add CRUD routes and add axtions routes
247
+ """
248
+
249
+ # read datas
250
+ if self._rights.get_strict_right("read") is not False:
251
+ # GET /<_id>
252
+ log.debug(f"Add routes GET {my_path}/coll/{self.name}/<string:_id>")
253
+
254
+ flask_app.add_url_rule(
255
+ f"{my_path}/coll/{self.name}/<string:_id>",
256
+ f"get_{self.name}",
257
+ methods=["GET"],
258
+ )
259
+ flask_app.view_functions[f"get_{self.name}"] = self.http_get_by_id
260
+
261
+ # GET / - The selection
262
+ log.debug(f"Add routes GET {my_path}/coll/{self.name}")
263
+ flask_app.add_url_rule(
264
+ f"{my_path}/coll/{self.name}", f"select_{self.name}", methods=["GET"]
265
+ )
266
+ flask_app.view_functions[f"select_{self.name}"] = self.filtering
267
+
268
+ # POST / Create data
269
+ if self._rights.get_strict_right("create") is not False:
270
+ log.debug(f"Add routes POST {my_path}/coll/{self.name}")
271
+ flask_app.add_url_rule(
272
+ f"{my_path}/coll/{self.name}", f"create_{self.name}", methods=["POST"]
273
+ )
274
+ flask_app.view_functions[f"create_{self.name}"] = self.http_create
275
+
276
+ # PUT /<_id> Modify Data
277
+ if self._rights.get_strict_right("modify") is not False:
278
+ log.debug(f"Add routes PUT {my_path}/coll/{self.name}/<string:_id>")
279
+ flask_app.add_url_rule(
280
+ f"{my_path}/coll/{self.name}/<string:_id>",
281
+ f"put_{self.name}",
282
+ methods=["PUT"],
283
+ )
284
+ flask_app.view_functions[f"put_{self.name}"] = self.http_modify
285
+
286
+ # PATCH /<_id> Modify Data
287
+ if self._rights.get_strict_right("modify") is not False:
288
+ log.debug(f"Add routes PATCH {my_path}/coll/{self.name}/<string:_id>")
289
+ flask_app.add_url_rule(
290
+ f"{my_path}/coll/{self.name}/<string:_id>",
291
+ f"patch_one_{self.name}",
292
+ methods=["PATCH"],
293
+ )
294
+ flask_app.view_functions[f"patch_one_{self.name}"] = self.http_patch_one
295
+
296
+ # DELETE /<_id> Delete Data
297
+ if self._rights.get_strict_right("delete") is not False:
298
+ log.debug(f"Add routes DELETE {my_path}/coll/{self.name}/<string:_id>")
299
+ flask_app.add_url_rule(
300
+ f"{my_path}/coll/{self.name}/<string:_id>",
301
+ f"delete_{self.name}",
302
+ methods=["DELETE"],
303
+ )
304
+ flask_app.view_functions[f"delete_{self.name}"] = self.http_delete
305
+
306
+ # CHECK /<_id> Check values
307
+ if self._rights.get_strict_right("read") is not False:
308
+ log.debug(f"Add routes POST {my_path}/check/{self.name}/<string:_id>")
309
+ flask_app.add_url_rule(
310
+ f"{my_path}/coll/{self.name}/<string:_id>",
311
+ f"delete_{self.name}",
312
+ methods=["DELETE"],
313
+ )
314
+ flask_app.view_functions[f"delete_{self.name}"] = self.http_delete
315
+
316
+ @error_to_http_handler
317
+ def http_get_by_id(self, _id: str):
318
+ """
319
+ GET HTTP
320
+ """
321
+ query = request.args
322
+ _view = query.get("_view", "client")
323
+
324
+ obj = self.new_item()
325
+ obj.load(_id)
326
+
327
+ log.debug(f"get by _id {_id} in {self.name} in view {_view}")
328
+ return (json.dumps(obj.get_view(_view), cls=StrictoEncoder), 200)
329
+
330
+ @error_to_http_handler
331
+ def filtering(self):
332
+ """
333
+ SELECT HTTP
334
+ """
335
+ query = request.args
336
+ _page = int(query.get("_page", 10))
337
+ _skip = int(query.get("_skip", 0))
338
+ _view = query.get("_view", "client")
339
+
340
+ match_filter = multidict_to_filter(query)
341
+
342
+ log.debug(f"filtering {self.name} with filter={match_filter}")
343
+
344
+ result = self.select(None, match_filter, _view, _page, _skip)
345
+ log.debug(
346
+ f"select in {self.name} {match_filter} {_view}/{_page} skip {_skip} -> {result}"
347
+ )
348
+
349
+ return (json.dumps(result, cls=StrictoEncoder), 200)
350
+
351
+ @error_to_http_handler
352
+ def http_create(self):
353
+ """
354
+ POST HTTP -> creation
355
+ """
356
+ query = request.args
357
+ _view = query.get("_view", "client")
358
+
359
+ log.debug(f"post {type(request.json)} {request.json}")
360
+ obj = self.create(request.json)
361
+
362
+ log.debug(f"create {obj._id} in {self.name} in view {_view}")
363
+ return (json.dumps(obj.get_view(_view), cls=StrictoEncoder), 200)
364
+
365
+ @error_to_http_handler
366
+ def http_modify(self, _id: str):
367
+ """
368
+ PUT HTTP -> modification of an object
369
+ """
370
+ query = request.args
371
+ _view = query.get("_view", "client")
372
+
373
+ log.debug(f"session {session.keys()}")
374
+
375
+ obj = self.new_item()
376
+ obj.load(_id)
377
+ obj.set(request.json)
378
+ obj.save()
379
+
380
+ return (json.dumps(obj.get_view(_view), cls=StrictoEncoder), 200)
381
+
382
+ @error_to_http_handler
383
+ def http_delete(self, _id: str):
384
+ """
385
+ DELETE HTTP -> deletion
386
+ """
387
+
388
+ obj = self.new_item()
389
+ obj.load(_id)
390
+ obj.delete()
391
+
392
+ return ("deleted", 200)
393
+
394
+ @error_to_http_handler
395
+ def http_patch_one(self, _id: str):
396
+ """
397
+ PATCH HTTP -> patch of an object
398
+ """
399
+ query = request.args
400
+ _view = query.get("_view", "client")
401
+
402
+ patch_list = request.json if isinstance(request.json, list) else [request.json]
403
+
404
+ obj = self.new_item()
405
+ obj.load(_id)
406
+
407
+ # apply patches
408
+ for p in patch_list:
409
+ patch = Patch()
410
+ patch.set(p)
411
+ obj.patch(patch.op, patch.path, patch.value)
412
+
413
+ obj.save()
414
+
415
+ return (json.dumps(obj.get_view(_view), cls=StrictoEncoder), 200)