viur-core 3.9.0.dev3__tar.gz → 3.9.0.dev4__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.
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/PKG-INFO +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/__init__.py +3 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/__init__.py +5 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/base.py +25 -2
- viur_core-3.9.0.dev4/src/viur/core/bones/code.py +95 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/email.py +4 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/relational.py +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/string.py +4 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/config.py +3 -0
- viur_core-3.9.0.dev4/src/viur/core/contrib/__init__.py +43 -0
- viur_core-3.9.0.dev4/src/viur/core/contrib/loginkey.py +115 -0
- viur_core-3.9.0.dev4/src/viur/core/contrib/ratelimit.py +123 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/cache.py +1 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/transport.py +33 -8
- viur_core-3.9.0.dev4/src/viur/core/modules/email.py +98 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/file.py +2 -2
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/script.py +1 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/user.py +3 -3
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/vi/__init__.py +25 -2
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/request.py +55 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/session.py +28 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/skeleton.py +4 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/tasks.py +2 -8
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/utils/__init__.py +4 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/PKG-INFO +1 -1
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/SOURCES.txt +7 -0
- viur_core-3.9.0.dev4/tests/test_decorators.py +271 -0
- viur_core-3.9.0.dev4/tests/test_errors.py +122 -0
- viur_core-3.9.0.dev4/tests/test_utils.py +299 -0
- viur_core-3.9.0.dev3/tests/test_utils.py +0 -156
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/LICENSE +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/README.md +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/pyproject.toml +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/setup.cfg +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/boolean.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/captcha.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/numeric.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/randomslice.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/record.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/spatial.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/text.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/cache.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/query.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/types.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/db/utils.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/module.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/history.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/modules/translation.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/skelmodule.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/render/json/default.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/securityheaders.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/__init__.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/instance.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/meta.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/skeleton/utils.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/tasks.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/requires.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/src/viur_core.egg-info/top_level.txt +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/tests/test_config.py +0 -0
- {viur_core-3.9.0.dev3 → viur_core-3.9.0.dev4}/tests/test_db.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viur-core
|
|
3
|
-
Version: 3.9.0.
|
|
3
|
+
Version: 3.9.0.dev4
|
|
4
4
|
Summary: The core component of ViUR, a development framework for Google App Engine
|
|
5
5
|
Author-email: Mausbrand Informationssysteme GmbH <devs@viur.dev>
|
|
6
6
|
Maintainer-email: Jan Max Meyer <jm@mausbrand.de>
|
|
@@ -18,6 +18,7 @@ from types import ModuleType
|
|
|
18
18
|
from viur.core import i18n, request, utils
|
|
19
19
|
from viur.core.config import conf
|
|
20
20
|
from viur.core.decorators import access, exposed, force_post, force_ssl, internal_exposed, skey
|
|
21
|
+
from viur.core.request import before_request, after_request
|
|
21
22
|
from viur.core.i18n import translate
|
|
22
23
|
from viur.core.module import Method, Module
|
|
23
24
|
import inspect
|
|
@@ -60,6 +61,8 @@ __all__ = [
|
|
|
60
61
|
"PeriodicTask",
|
|
61
62
|
# Decorators
|
|
62
63
|
"access",
|
|
64
|
+
"after_request",
|
|
65
|
+
"before_request",
|
|
63
66
|
"exposed",
|
|
64
67
|
"force_post",
|
|
65
68
|
"force_ssl",
|
|
@@ -13,6 +13,7 @@ from .base import (
|
|
|
13
13
|
UniqueValue,
|
|
14
14
|
)
|
|
15
15
|
from .boolean import BooleanBone
|
|
16
|
+
from .code import CodeBone, JinjaBone, LogicsBone, PythonBone
|
|
16
17
|
from .captcha import CaptchaBone
|
|
17
18
|
from .color import ColorBone
|
|
18
19
|
from .credential import CredentialBone
|
|
@@ -51,6 +52,10 @@ __all = [
|
|
|
51
52
|
"BaseBone",
|
|
52
53
|
"BooleanBone",
|
|
53
54
|
"CaptchaBone",
|
|
55
|
+
"CodeBone",
|
|
56
|
+
"JinjaBone",
|
|
57
|
+
"LogicsBone",
|
|
58
|
+
"PythonBone",
|
|
54
59
|
"CloneBehavior",
|
|
55
60
|
"CloneStrategy",
|
|
56
61
|
"ColorBone",
|
|
@@ -798,18 +798,23 @@ class BaseBone(object):
|
|
|
798
798
|
if self.languages and isinstance(self.required, (list, tuple)):
|
|
799
799
|
missing = set(self.required).difference(filled_languages)
|
|
800
800
|
if missing:
|
|
801
|
-
|
|
801
|
+
result_errors = [
|
|
802
802
|
ReadFromClientError(ReadFromClientErrorSeverity.Empty, fieldPath=[lang])
|
|
803
803
|
for lang in missing
|
|
804
804
|
]
|
|
805
|
+
self.after_from_client(skel, name, result_errors)
|
|
806
|
+
return result_errors or None
|
|
805
807
|
|
|
806
808
|
if isEmpty:
|
|
807
|
-
|
|
809
|
+
result_errors = [ReadFromClientError(ReadFromClientErrorSeverity.Empty)]
|
|
810
|
+
self.after_from_client(skel, name, result_errors)
|
|
811
|
+
return result_errors or None
|
|
808
812
|
|
|
809
813
|
# Check multiple constraints on demand
|
|
810
814
|
if self.multiple and isinstance(self.multiple, MultipleConstraints):
|
|
811
815
|
errors.extend(self._validate_multiple_contraints(self.multiple, skel, name))
|
|
812
816
|
|
|
817
|
+
self.after_from_client(skel, name, errors)
|
|
813
818
|
return errors or None
|
|
814
819
|
|
|
815
820
|
def _get_single_destinct_hash(self, value) -> t.Any:
|
|
@@ -1387,6 +1392,24 @@ class BaseBone(object):
|
|
|
1387
1392
|
"""
|
|
1388
1393
|
pass
|
|
1389
1394
|
|
|
1395
|
+
def after_from_client(self, skel: "SkeletonInstance", name: str, errors: list[ReadFromClientError]) -> None:
|
|
1396
|
+
"""
|
|
1397
|
+
Called at the end of :meth:`fromClient` after ``skel[name]`` has been set and all
|
|
1398
|
+
validation (including multiple-constraints) has run.
|
|
1399
|
+
|
|
1400
|
+
Override to post-process or normalize ``skel[name]`` in-place, or to add/remove
|
|
1401
|
+
entries from ``errors``. Always called when the field was part of the submitted data
|
|
1402
|
+
(i.e. ``skel[name]`` has been written), regardless of whether errors occurred.
|
|
1403
|
+
The ``NotSet`` early-return (field absent from request) is the only case where this
|
|
1404
|
+
hook is *not* called.
|
|
1405
|
+
|
|
1406
|
+
:param skel: The skeleton instance whose bone value was just read.
|
|
1407
|
+
:param name: The attribute name of this bone within the skeleton.
|
|
1408
|
+
:param errors: Mutable list of :class:`ReadFromClientError` collected so far.
|
|
1409
|
+
Changes here affect the return value of :meth:`fromClient`.
|
|
1410
|
+
"""
|
|
1411
|
+
pass
|
|
1412
|
+
|
|
1390
1413
|
def clone_value(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> None:
|
|
1391
1414
|
"""Clone / Set the value for this bone depending on :attr:`clone_behavior`"""
|
|
1392
1415
|
match self.clone_behavior.strategy:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import jinja2
|
|
3
|
+
import logics
|
|
4
|
+
from viur.core.bones.raw import RawBone
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CodeBone(RawBone):
|
|
8
|
+
"""
|
|
9
|
+
Stores source code with optional language-specific syntax validation.
|
|
10
|
+
|
|
11
|
+
The ``syntax`` parameter sets the type suffix used by the frontend for syntax
|
|
12
|
+
highlighting, e.g. ``syntax="python"`` yields ``type = "raw.code.python"``.
|
|
13
|
+
``type_suffix`` can override this to an arbitrary suffix.
|
|
14
|
+
|
|
15
|
+
Neither ``multiple`` nor ``languages`` are supported.
|
|
16
|
+
Setting ``validate=False`` disables any syntax validation in subclasses.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
type = "raw.code"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
indexed: bool = False,
|
|
25
|
+
languages=None,
|
|
26
|
+
multiple: bool = False,
|
|
27
|
+
syntax: str | None = None,
|
|
28
|
+
type_suffix: str = "",
|
|
29
|
+
validate: bool = True,
|
|
30
|
+
**kwargs,
|
|
31
|
+
):
|
|
32
|
+
assert not multiple, "CodeBone does not support multiple values"
|
|
33
|
+
assert not languages, "CodeBone does not support language variants"
|
|
34
|
+
self.syntax = syntax
|
|
35
|
+
self.validate = validate
|
|
36
|
+
if self.syntax:
|
|
37
|
+
type_suffix = type_suffix or syntax
|
|
38
|
+
super().__init__(indexed=indexed, type_suffix=type_suffix, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LogicsBone(CodeBone):
|
|
42
|
+
"""
|
|
43
|
+
Validates its value as a Logics expression (https://github.com/viur-framework/logics).
|
|
44
|
+
Uses Python syntax highlighting in the frontend.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, *, syntax: str = "logics", **kwargs):
|
|
48
|
+
super().__init__(syntax=syntax, type_suffix="python", **kwargs)
|
|
49
|
+
|
|
50
|
+
def singleValueFromClient(self, value, skel, bone_name, client_data):
|
|
51
|
+
if value := str(value or "").strip():
|
|
52
|
+
value += "\n"
|
|
53
|
+
return super().singleValueFromClient(value, skel, bone_name, client_data)
|
|
54
|
+
|
|
55
|
+
def isInvalid(self, value):
|
|
56
|
+
if self.validate and value:
|
|
57
|
+
try:
|
|
58
|
+
logics.Logics(value)
|
|
59
|
+
except logics.ParseException as e:
|
|
60
|
+
return str(e).replace("&eof", "end-of-expression")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JinjaBone(CodeBone):
|
|
64
|
+
"""
|
|
65
|
+
Validates its value as a Jinja2 template.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, syntax: str = "jinja2", **kwargs):
|
|
69
|
+
super().__init__(syntax=syntax, **kwargs)
|
|
70
|
+
|
|
71
|
+
def isInvalid(self, value):
|
|
72
|
+
if self.validate and value:
|
|
73
|
+
env = jinja2.Environment()
|
|
74
|
+
try:
|
|
75
|
+
env.parse(value)
|
|
76
|
+
except jinja2.TemplateSyntaxError as e:
|
|
77
|
+
return f"Syntax error in line {e.lineno}: {e.message}"
|
|
78
|
+
except jinja2.TemplateError as e:
|
|
79
|
+
return f"General error: {e}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PythonBone(CodeBone):
|
|
83
|
+
"""
|
|
84
|
+
Validates its value as Python source code.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *, syntax: str = "python", **kwargs):
|
|
88
|
+
super().__init__(syntax=syntax, **kwargs)
|
|
89
|
+
|
|
90
|
+
def isInvalid(self, value):
|
|
91
|
+
if self.validate and value:
|
|
92
|
+
try:
|
|
93
|
+
ast.parse(value)
|
|
94
|
+
except SyntaxError as e:
|
|
95
|
+
return f"Syntax error in line {e.lineno}: {e.msg}"
|
|
@@ -45,6 +45,10 @@ class EmailBone(StringBone):
|
|
|
45
45
|
assert account and subDomain and tld
|
|
46
46
|
assert subDomain[0] != "."
|
|
47
47
|
assert len(account) <= 64
|
|
48
|
+
# RFC 5321: local part must not start/end with a dot or contain consecutive dots
|
|
49
|
+
assert not account.startswith(".")
|
|
50
|
+
assert not account.endswith(".")
|
|
51
|
+
assert ".." not in account
|
|
48
52
|
except (ValueError, AssertionError):
|
|
49
53
|
is_valid = False
|
|
50
54
|
|
|
@@ -1107,7 +1107,7 @@ class RelationalBone(BaseBone):
|
|
|
1107
1107
|
|
|
1108
1108
|
ref_skel_cache, using_skel_cache = self._getSkels()
|
|
1109
1109
|
for idx, lang, value in self.iter_bone_value(skel, name):
|
|
1110
|
-
if value
|
|
1110
|
+
if not value:
|
|
1111
1111
|
continue
|
|
1112
1112
|
if value["dest"]:
|
|
1113
1113
|
get_values(ref_skel_cache, value["dest"])
|
|
@@ -6,7 +6,7 @@ import typing as t
|
|
|
6
6
|
import warnings
|
|
7
7
|
from numbers import Number
|
|
8
8
|
|
|
9
|
-
from viur.core import current, db, utils
|
|
9
|
+
from viur.core import conf, current, db, utils
|
|
10
10
|
from .base import ReadFromClientError, ReadFromClientErrorSeverity
|
|
11
11
|
from .raw import RawBone
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ class StringBone(RawBone):
|
|
|
29
29
|
max_length: int | None = 254,
|
|
30
30
|
min_length: int | None = None,
|
|
31
31
|
natural_sorting: bool | t.Callable = False,
|
|
32
|
-
escape_html: bool =
|
|
32
|
+
escape_html: bool | None = None,
|
|
33
33
|
**kwargs
|
|
34
34
|
):
|
|
35
35
|
"""
|
|
@@ -46,6 +46,7 @@ class StringBone(RawBone):
|
|
|
46
46
|
that creates the value for the index property.
|
|
47
47
|
:param escape_html: Replace some characters in the string with HTML-safe sequences with
|
|
48
48
|
using :meth:`utils.string.escape` for safe use in HTML.
|
|
49
|
+
Defaults to :attr:`conf.bone_string_escape_html` if not set explicitly.
|
|
49
50
|
:param kwargs: Inherited arguments from the BaseBone.
|
|
50
51
|
"""
|
|
51
52
|
# fixme: Remove in viur-core >= 4
|
|
@@ -71,7 +72,7 @@ class StringBone(RawBone):
|
|
|
71
72
|
elif not natural_sorting:
|
|
72
73
|
self.natural_sorting = None
|
|
73
74
|
# else: keep self.natural_sorting as is
|
|
74
|
-
self.escape_html = escape_html
|
|
75
|
+
self.escape_html = conf.bone_string_escape_html if escape_html is None else escape_html
|
|
75
76
|
|
|
76
77
|
def type_coerce_single_value(self, value: t.Any) -> str:
|
|
77
78
|
"""Convert a value to a string (if not already)
|
|
@@ -794,6 +794,9 @@ class Conf(ConfigType):
|
|
|
794
794
|
bone_boolean_str2true: Multiple[str | int] = ("true", "yes", "1")
|
|
795
795
|
"""Allowed values that define a str to evaluate to true"""
|
|
796
796
|
|
|
797
|
+
bone_string_escape_html: bool = True
|
|
798
|
+
"""Default escape_html setting for StringBone. Set to False to disable HTML escaping globally."""
|
|
799
|
+
|
|
797
800
|
bone_html_default_allow: "HtmlBoneConfiguration" = {
|
|
798
801
|
"validTags": [
|
|
799
802
|
"a",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viur.core.contrib — optional, reusable application-level components.
|
|
3
|
+
|
|
4
|
+
This package contains self-contained components that are commonly needed
|
|
5
|
+
but *not* required to run a ViUR application. Components are opt-in;
|
|
6
|
+
import only what you use.
|
|
7
|
+
|
|
8
|
+
Available modules
|
|
9
|
+
-----------------
|
|
10
|
+
loginkey
|
|
11
|
+
:class:`~viur.core.contrib.loginkey.IndexedCredentialBone` and
|
|
12
|
+
:class:`~viur.core.contrib.loginkey.LoginKey` — a
|
|
13
|
+
:class:`~viur.core.modules.user.UserPrimaryAuthentication` that
|
|
14
|
+
authenticates users via a secret token stored in a Datastore-indexed
|
|
15
|
+
:class:`~viur.core.bones.CredentialBone`. Suitable for "magic link"
|
|
16
|
+
style logins or machine-to-machine auth.
|
|
17
|
+
|
|
18
|
+
Usage example::
|
|
19
|
+
|
|
20
|
+
from viur.core.modules.user import User
|
|
21
|
+
from viur.core.contrib.loginkey import LoginKey
|
|
22
|
+
|
|
23
|
+
class MyUser(User):
|
|
24
|
+
authenticationProviders = [LoginKey, ...]
|
|
25
|
+
|
|
26
|
+
ratelimit
|
|
27
|
+
:class:`~viur.core.contrib.ratelimit.RequestRateLimit` — a
|
|
28
|
+
:class:`~viur.core.request.RequestValidator` that enforces per-IP /
|
|
29
|
+
per-user request-rate limits using App Engine Memcache. Suitable for
|
|
30
|
+
global rate-limiting and basic DDoS mitigation at the WSGI boundary.
|
|
31
|
+
|
|
32
|
+
Usage example::
|
|
33
|
+
|
|
34
|
+
from viur.core.request import Router
|
|
35
|
+
from viur.core.contrib.ratelimit import RequestRateLimit, TimeWindow
|
|
36
|
+
|
|
37
|
+
Router.requestValidators.append(
|
|
38
|
+
RequestRateLimit(
|
|
39
|
+
rate_for_guests=TimeWindow(limit=200, time_window=60),
|
|
40
|
+
rate_for_users=TimeWindow(limit=500, time_window=60),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token-based ("magic link") primary authentication for ViUR user modules.
|
|
3
|
+
|
|
4
|
+
``LoginKey`` authenticates a user by a secret token stored as an indexed
|
|
5
|
+
``CredentialBone`` on the user skeleton. The caller submits the token as a
|
|
6
|
+
POST parameter; the handler looks up the matching user, validates the account
|
|
7
|
+
state, and completes the authentication flow.
|
|
8
|
+
|
|
9
|
+
Typical use cases include magic-link email logins, CLI tool authentication,
|
|
10
|
+
and service-to-service auth where a shared secret is acceptable.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
from viur.core.modules.user import User
|
|
15
|
+
from viur.core.contrib.loginkey import LoginKey
|
|
16
|
+
|
|
17
|
+
class MyUser(User):
|
|
18
|
+
authenticationProviders = [LoginKey, ...]
|
|
19
|
+
|
|
20
|
+
.. warning::
|
|
21
|
+
An indexed :class:`~viur.core.bones.CredentialBone` allows any caller
|
|
22
|
+
with Datastore read access to enumerate users by key. Only deploy this
|
|
23
|
+
in environments where that access is appropriately restricted, and always
|
|
24
|
+
use long (≥ 32 char), randomly generated tokens.
|
|
25
|
+
"""
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
from viur.core import current, errors
|
|
29
|
+
from viur.core.bones import CredentialBone
|
|
30
|
+
from viur.core.decorators import exposed, force_post, force_ssl, skey
|
|
31
|
+
from viur.core.modules.user import Status, UserPrimaryAuthentication
|
|
32
|
+
from viur.core.ratelimit import RateLimit
|
|
33
|
+
from viur.core.skeleton import SkeletonInstance
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IndexedCredentialBone(CredentialBone):
|
|
39
|
+
"""A :class:`~viur.core.bones.CredentialBone` that is always Datastore-indexed.
|
|
40
|
+
|
|
41
|
+
Regular ``CredentialBone`` values are excluded from indexes for security.
|
|
42
|
+
This subclass forces indexing so that the value can be used as a filter
|
|
43
|
+
criterion (e.g. ``filter("login_key =", token)``).
|
|
44
|
+
|
|
45
|
+
.. note::
|
|
46
|
+
Accepting an indexed credential is a deliberate trade-off: it enables
|
|
47
|
+
server-side token lookup at the cost of exposing the value to anyone
|
|
48
|
+
with Datastore read access. Only use this when that trade-off is
|
|
49
|
+
explicitly acceptable.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool:
|
|
53
|
+
skel.dbEntity.exclude_from_indexes.discard(name) # force index even though it's a credential
|
|
54
|
+
if name in skel.accessedValues and skel.accessedValues[name]:
|
|
55
|
+
skel.dbEntity[name] = skel.accessedValues[name]
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LoginKey(UserPrimaryAuthentication):
|
|
61
|
+
"""Primary authentication via a secret login token.
|
|
62
|
+
|
|
63
|
+
The token is stored in a ``login_key`` bone on the user skeleton
|
|
64
|
+
(added automatically by :meth:`patch_user_skel`). Failed attempts are
|
|
65
|
+
rate-limited per IP address; successful logins are *not* counted against
|
|
66
|
+
the quota.
|
|
67
|
+
|
|
68
|
+
:cvar METHOD_NAME: HTTP header name used to identify this auth method.
|
|
69
|
+
:cvar loginRateLimit: Allows 12 failed attempts per minute per IP.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
METHOD_NAME = "X-AUTH-LOGINKEY"
|
|
73
|
+
NAME = "LoginKey"
|
|
74
|
+
|
|
75
|
+
# 12 failed attempts per minute, IP-based
|
|
76
|
+
loginRateLimit = RateLimit("user.loginkey", 12, 1, "ip")
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def patch_user_skel(cls, skel_cls):
|
|
80
|
+
skel_cls.login_key = IndexedCredentialBone(
|
|
81
|
+
descr="LoginKey",
|
|
82
|
+
params={"category": "Authentication"},
|
|
83
|
+
min_length=32,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@exposed
|
|
87
|
+
@force_ssl
|
|
88
|
+
@force_post
|
|
89
|
+
@skey()
|
|
90
|
+
def login(self, *, key: str, **kwargs):
|
|
91
|
+
if current.user.get():
|
|
92
|
+
return self._user_module.render.loginSucceeded()
|
|
93
|
+
|
|
94
|
+
self.loginRateLimit.assertQuotaIsAvailable()
|
|
95
|
+
|
|
96
|
+
user_skel = self._user_module.baseSkel()
|
|
97
|
+
user_skel = user_skel.all().filter("login_key =", key).getSkel()
|
|
98
|
+
|
|
99
|
+
is_okay = user_skel is not None
|
|
100
|
+
logger.debug(f"user found: {is_okay=}")
|
|
101
|
+
|
|
102
|
+
is_okay = is_okay and (user_skel["status"] or 0) >= Status.ACTIVE.value
|
|
103
|
+
logger.debug(f"account active: {is_okay=}")
|
|
104
|
+
|
|
105
|
+
is_okay = is_okay and len(str(user_skel.dbEntity["login_key"])) >= 32
|
|
106
|
+
logger.debug(f"key length ok: {is_okay=}")
|
|
107
|
+
|
|
108
|
+
is_okay = is_okay and ("root" not in user_skel["access"])
|
|
109
|
+
logger.debug(f"not root: {is_okay=}")
|
|
110
|
+
|
|
111
|
+
if not is_okay:
|
|
112
|
+
self.loginRateLimit.decrementQuota() # only failed attempts count
|
|
113
|
+
raise errors.Unauthorized()
|
|
114
|
+
|
|
115
|
+
return self.next_or_finish(user_skel)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request-level rate limiter using App Engine Memcache.
|
|
3
|
+
|
|
4
|
+
Registers as a :class:`~viur.core.request.RequestValidator` and therefore
|
|
5
|
+
runs *before* any session, routing, or handler logic — making it the
|
|
6
|
+
earliest possible place to shed excess traffic.
|
|
7
|
+
|
|
8
|
+
Guests are identified by their IP address (IPv6 addresses are bucketed into
|
|
9
|
+
/64 prefixes so that a single host cannot trivially rotate around the limit).
|
|
10
|
+
Authenticated users are identified by their Datastore user key.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
from viur.core.request import Router
|
|
15
|
+
from viur.core.contrib.ratelimit import RequestRateLimit, TimeWindow
|
|
16
|
+
|
|
17
|
+
Router.requestValidators.append(
|
|
18
|
+
RequestRateLimit(
|
|
19
|
+
rate_for_guests=TimeWindow(limit=200, time_window=60),
|
|
20
|
+
rate_for_users=TimeWindow(limit=500, time_window=60),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
import dataclasses
|
|
25
|
+
import ipaddress
|
|
26
|
+
import logging
|
|
27
|
+
import time
|
|
28
|
+
import typing as t
|
|
29
|
+
|
|
30
|
+
from google.appengine.api.memcache import Client
|
|
31
|
+
|
|
32
|
+
from viur.core import current
|
|
33
|
+
from viur.core.request import RequestValidator, Router
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_memcache = Client()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclasses.dataclass(frozen=True)
|
|
41
|
+
class TimeWindow:
|
|
42
|
+
"""Rate-limit budget for a single time window.
|
|
43
|
+
|
|
44
|
+
:param limit: Maximum number of requests allowed within *time_window*.
|
|
45
|
+
:param time_window: Length of the window in seconds.
|
|
46
|
+
"""
|
|
47
|
+
limit: int
|
|
48
|
+
time_window: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RequestRateLimit(RequestValidator):
|
|
52
|
+
"""Global HTTP request rate limiter.
|
|
53
|
+
|
|
54
|
+
Enforces separate budgets for anonymous (guest) and authenticated
|
|
55
|
+
requests. When the budget is exceeded the validator returns HTTP 429
|
|
56
|
+
and sets the ``Retry-After`` header so clients know when to retry.
|
|
57
|
+
|
|
58
|
+
:param rate_for_guests: Budget applied to unauthenticated requests.
|
|
59
|
+
:param rate_for_users: Budget applied to authenticated requests.
|
|
60
|
+
:param namespace: Memcache namespace used for all rate-limit keys.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name = "RequestRateLimit"
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
rate_for_guests: TimeWindow = TimeWindow(limit=1000, time_window=60),
|
|
68
|
+
rate_for_users: TimeWindow = TimeWindow(limit=2000, time_window=60),
|
|
69
|
+
namespace: str = "viur_rate_limit",
|
|
70
|
+
):
|
|
71
|
+
self.rate_for_guests = rate_for_guests
|
|
72
|
+
self.rate_for_users = rate_for_users
|
|
73
|
+
self.namespace = namespace
|
|
74
|
+
|
|
75
|
+
def validate(self, request: "Router") -> t.Optional[tuple[int, str, str]]:
|
|
76
|
+
if request.is_deferred:
|
|
77
|
+
return None # Task Queue requests are always allowed
|
|
78
|
+
|
|
79
|
+
if user := current.user.get():
|
|
80
|
+
client_id = str(user["key"])
|
|
81
|
+
rate = self.rate_for_users
|
|
82
|
+
else:
|
|
83
|
+
client_id = self._get_request_ip()
|
|
84
|
+
rate = self.rate_for_guests
|
|
85
|
+
|
|
86
|
+
current_time = time.time() / rate.time_window
|
|
87
|
+
key = f"rate_limit:{client_id}:{int(current_time)}"
|
|
88
|
+
|
|
89
|
+
count = _memcache.get(key, namespace=self.namespace)
|
|
90
|
+
logger.debug(f"rate limit check: {client_id=} {count=} limit={rate.limit}")
|
|
91
|
+
|
|
92
|
+
if count is None:
|
|
93
|
+
_memcache.add(key, 1, time=rate.time_window, namespace=self.namespace)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
if count < rate.limit:
|
|
97
|
+
_memcache.incr(key, initial_value=1, namespace=self.namespace)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Budget exhausted — tell the client when the current window expires.
|
|
101
|
+
seconds_into_window = (current_time - int(current_time)) * rate.time_window
|
|
102
|
+
retry_after = int(rate.time_window - seconds_into_window)
|
|
103
|
+
request.response.headers["Retry-After"] = str(retry_after)
|
|
104
|
+
return 429, "Too Many Requests", "Too Many Requests. Please try again later."
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _get_request_ip() -> str:
|
|
108
|
+
"""Return a stable client identifier derived from the remote address.
|
|
109
|
+
|
|
110
|
+
IPv4 addresses are returned as-is. For IPv6 the /64 network prefix
|
|
111
|
+
is returned so that a single host cannot trivially rotate its
|
|
112
|
+
interface identifier to bypass the limit.
|
|
113
|
+
"""
|
|
114
|
+
raw = current.request.get().request.remote_addr
|
|
115
|
+
ip = ipaddress.ip_address(raw)
|
|
116
|
+
|
|
117
|
+
if isinstance(ip, ipaddress.IPv4Address):
|
|
118
|
+
return str(ip)
|
|
119
|
+
|
|
120
|
+
if isinstance(ip, ipaddress.IPv6Address):
|
|
121
|
+
return str(ipaddress.IPv6Network((ip, 64), strict=False))
|
|
122
|
+
|
|
123
|
+
raise NotImplementedError(f"Unsupported IP version: {ip!r}")
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
|
|
3
|
-
from google.appengine.ext.testbed import Testbed
|
|
4
|
-
|
|
5
2
|
import logging
|
|
6
3
|
import sys
|
|
7
4
|
import typing as t
|
|
@@ -169,6 +166,7 @@ def check_for_memcache() -> bool:
|
|
|
169
166
|
def init_testbed() -> None:
|
|
170
167
|
global TESTBED
|
|
171
168
|
if TESTBED is None and conf.instance.is_dev_server and conf.db.memcache_client:
|
|
169
|
+
from google.appengine.ext.testbed import Testbed
|
|
172
170
|
TESTBED = Testbed()
|
|
173
171
|
TESTBED.activate()
|
|
174
172
|
TESTBED.init_memcache_stub()
|
|
@@ -49,9 +49,15 @@ def get(keys: t.Union[Key, t.List[Key]]) -> t.Union[t.List[Entity], Entity, None
|
|
|
49
49
|
if isinstance(keys, (list, set, tuple)):
|
|
50
50
|
res_list = list(__client__.get_multi(keys))
|
|
51
51
|
res_list.sort(key=lambda k: keys.index(k.key) if k else -1)
|
|
52
|
+
if conf.debug.trace_queries:
|
|
53
|
+
found = sum(1 for r in res_list if r is not None)
|
|
54
|
+
logging.info(f"db.get: {found}/{len(keys)} entities found")
|
|
52
55
|
return res_list
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
res = __client__.get(keys)
|
|
58
|
+
if conf.debug.trace_queries:
|
|
59
|
+
logging.info(f"db.get({keys}): {'found' if res is not None else 'not found'}")
|
|
60
|
+
return res
|
|
55
61
|
|
|
56
62
|
|
|
57
63
|
@deprecated(version="3.8.0", reason="Use 'db.get' instead")
|
|
@@ -67,9 +73,15 @@ def put(entities: t.Union[Entity, t.List[Entity]]):
|
|
|
67
73
|
"""
|
|
68
74
|
_write_to_access_log(entities)
|
|
69
75
|
if isinstance(entities, Entity):
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
res = __client__.put(entities)
|
|
77
|
+
if conf.debug.trace_queries:
|
|
78
|
+
logging.info(f"db.put: saved {entities.key}")
|
|
79
|
+
return res
|
|
80
|
+
|
|
81
|
+
res = __client__.put_multi(entities=entities)
|
|
82
|
+
if conf.debug.trace_queries:
|
|
83
|
+
logging.info(f"db.put: saved {len(entities)} entities")
|
|
84
|
+
return res
|
|
73
85
|
|
|
74
86
|
|
|
75
87
|
@deprecated(version="3.8.0", reason="Use 'db.put' instead")
|
|
@@ -82,12 +94,17 @@ def delete(keys: t.Union[Entity, t.List[Entity], Key, t.List[Key]]):
|
|
|
82
94
|
Deletes the entities with the given key(s) from the datastore.
|
|
83
95
|
:param keys: A Key (or a t.List of Keys) to delete
|
|
84
96
|
"""
|
|
85
|
-
|
|
86
97
|
_write_to_access_log(keys)
|
|
87
98
|
if not isinstance(keys, (set, list, tuple)):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
res = __client__.delete(keys)
|
|
100
|
+
if conf.debug.trace_queries:
|
|
101
|
+
logging.info(f"db.delete: deleted {keys}")
|
|
102
|
+
return res
|
|
103
|
+
|
|
104
|
+
res = __client__.delete_multi(keys)
|
|
105
|
+
if conf.debug.trace_queries:
|
|
106
|
+
logging.info(f"db.delete: deleted {len(keys)} keys")
|
|
107
|
+
return res
|
|
91
108
|
|
|
92
109
|
|
|
93
110
|
@deprecated(version="3.8.0", reason="Use 'db.delete' instead")
|
|
@@ -212,6 +229,14 @@ def run_single_filter(query: QueryDefinition, limit: int, keys_only: bool) -> t.
|
|
|
212
229
|
query.currentCursor = qryRes.next_page_token
|
|
213
230
|
if hasInvertedOrderings:
|
|
214
231
|
res.reverse()
|
|
232
|
+
|
|
233
|
+
if conf.debug.trace_queries:
|
|
234
|
+
distinct_on = f" distinct on {query.distinct}" if query.distinct else ""
|
|
235
|
+
logging.debug(
|
|
236
|
+
f"Queried {query.kind} with filter {query.filters} and orders {query.orders}{distinct_on}."
|
|
237
|
+
f" Returned {len(res)} results"
|
|
238
|
+
)
|
|
239
|
+
|
|
215
240
|
return res
|
|
216
241
|
|
|
217
242
|
|