viur-core 3.9.0.dev1__tar.gz → 3.9.0.dev3__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.dev1 → viur_core-3.9.0.dev3}/PKG-INFO +1 -1
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/base.py +22 -17
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/boolean.py +7 -2
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/record.py +10 -5
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/relational.py +34 -13
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/string.py +7 -11
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/text.py +0 -19
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/cache.py +7 -6
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/file.py +5 -3
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/__init__.py +7 -5
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/PKG-INFO +1 -1
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/tests/test_utils.py +78 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/LICENSE +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/README.md +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/pyproject.toml +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/setup.cfg +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/captcha.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/email.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/numeric.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/randomslice.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/spatial.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/config.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/cache.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/query.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/transport.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/types.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/utils.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/module.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/history.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/script.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/translation.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/user.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/skelmodule.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/json/default.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/vi/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/request.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/securityheaders.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/session.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/__init__.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/instance.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/meta.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/skeleton.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/tasks.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/utils.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/tasks.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/SOURCES.txt +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/requires.txt +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/top_level.txt +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/tests/test_config.py +0 -0
- {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/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.dev3
|
|
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>
|
|
@@ -1293,10 +1293,11 @@ class BaseBone(object):
|
|
|
1293
1293
|
the list may contain more than one hashed value.
|
|
1294
1294
|
"""
|
|
1295
1295
|
|
|
1296
|
-
def
|
|
1296
|
+
def hash_value(value: str | int | float | db.Key) -> str:
|
|
1297
1297
|
h = hashlib.sha256()
|
|
1298
1298
|
h.update(str(value).encode("UTF-8"))
|
|
1299
1299
|
res = h.hexdigest()
|
|
1300
|
+
|
|
1300
1301
|
if isinstance(value, int | float):
|
|
1301
1302
|
return f"I-{res}"
|
|
1302
1303
|
elif isinstance(value, str):
|
|
@@ -1307,27 +1308,32 @@ class BaseBone(object):
|
|
|
1307
1308
|
def keyHash(key):
|
|
1308
1309
|
if key is None:
|
|
1309
1310
|
return "-"
|
|
1310
|
-
return f"{
|
|
1311
|
+
return f"{hash_value(key.kind)}-{hash_value(key.id_or_name)}-<{keyHash(key.parent)}>"
|
|
1311
1312
|
|
|
1312
1313
|
return f"K-{keyHash(value)}"
|
|
1314
|
+
|
|
1313
1315
|
raise NotImplementedError(f"Type {type(value)} can't be safely used in an uniquePropertyIndex")
|
|
1314
1316
|
|
|
1317
|
+
# zero/empty string and these should not be locked
|
|
1315
1318
|
if not value and not self.unique.lockEmpty:
|
|
1316
|
-
return []
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
# We have a multiple bone or multiple values here
|
|
1319
|
+
return []
|
|
1320
|
+
|
|
1321
|
+
# Always work with list of values
|
|
1320
1322
|
if not isinstance(value, list):
|
|
1321
1323
|
value = [value]
|
|
1322
|
-
|
|
1324
|
+
|
|
1325
|
+
values = [hash_value(val) for val in value]
|
|
1326
|
+
|
|
1323
1327
|
if self.unique.method == UniqueLockMethod.SameValue:
|
|
1324
|
-
#
|
|
1325
|
-
return
|
|
1328
|
+
# Lock each entry individually
|
|
1329
|
+
return values
|
|
1330
|
+
|
|
1326
1331
|
elif self.unique.method == UniqueLockMethod.SameSet:
|
|
1327
|
-
#
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1332
|
+
# Ignore the sort-order; so simply sort that list
|
|
1333
|
+
values.sort()
|
|
1334
|
+
|
|
1335
|
+
# Lock the value for that specific list (equals to UniqueLockMethod.SameList)
|
|
1336
|
+
return [hash_value(", ".join(values))]
|
|
1331
1337
|
|
|
1332
1338
|
def getUniquePropertyIndexValues(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> list[str]:
|
|
1333
1339
|
"""
|
|
@@ -1343,10 +1349,9 @@ class BaseBone(object):
|
|
|
1343
1349
|
"""
|
|
1344
1350
|
if self.compute:
|
|
1345
1351
|
self.serialize_compute(skel, name)
|
|
1346
|
-
|
|
1347
|
-
if
|
|
1348
|
-
|
|
1349
|
-
return self._hashValueForUniquePropertyIndex(val)
|
|
1352
|
+
|
|
1353
|
+
values = [value for _, _, value in self.iter_bone_value(skel, name) if value is not None]
|
|
1354
|
+
return self._hashValueForUniquePropertyIndex(values) if values else []
|
|
1350
1355
|
|
|
1351
1356
|
def getReferencedBlobs(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> set[str]:
|
|
1352
1357
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
|
|
3
3
|
from viur.core import conf, db, utils
|
|
4
|
-
from viur.core.bones.base import BaseBone
|
|
4
|
+
from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
|
|
5
5
|
|
|
6
6
|
DEFAULT_VALUE_T: t.TypeAlias = bool | None | list[bool] | dict[str, list[bool] | bool]
|
|
7
7
|
|
|
@@ -36,7 +36,12 @@ class BooleanBone(BaseBone):
|
|
|
36
36
|
raise ValueError("BooleanBone cannot be multiple")
|
|
37
37
|
|
|
38
38
|
def singleValueFromClient(self, value, skel, bone_name, client_data):
|
|
39
|
-
|
|
39
|
+
value = utils.parse.bool(value, conf.bone_boolean_str2true)
|
|
40
|
+
|
|
41
|
+
if err := self.isInvalid(value):
|
|
42
|
+
return value, [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)]
|
|
43
|
+
|
|
44
|
+
return value, None
|
|
40
45
|
|
|
41
46
|
def getEmptyValue(self):
|
|
42
47
|
"""
|
|
@@ -215,13 +215,18 @@ class RecordBone(BaseBone):
|
|
|
215
215
|
|
|
216
216
|
return result
|
|
217
217
|
|
|
218
|
-
def getUniquePropertyIndexValues(self,
|
|
218
|
+
def getUniquePropertyIndexValues(self, skel: "SkeletonInstance", name: str) -> list[str]:
|
|
219
219
|
"""
|
|
220
|
-
|
|
221
|
-
a key from the related skeleton being used (i.e., which fields to include and how).
|
|
222
|
-
|
|
220
|
+
Returns hashes over all fields of each record value, using the serialized form as canonical input.
|
|
223
221
|
"""
|
|
224
|
-
|
|
222
|
+
values = []
|
|
223
|
+
|
|
224
|
+
for _, _, using_skel in self.iter_bone_value(skel, name):
|
|
225
|
+
if using_skel is None:
|
|
226
|
+
continue
|
|
227
|
+
values.append(json.dumps(using_skel.dump(), sort_keys=True, default=str))
|
|
228
|
+
|
|
229
|
+
return self._hashValueForUniquePropertyIndex(values) if values else []
|
|
225
230
|
|
|
226
231
|
def structure(self) -> dict:
|
|
227
232
|
return super().structure() | {
|
|
@@ -544,7 +544,12 @@ class RelationalBone(BaseBone):
|
|
|
544
544
|
entity["viur_delayed_update_tag"] = now
|
|
545
545
|
entity["viur_relational_updateLevel"] = self.updateLevel.value
|
|
546
546
|
entity["viur_relational_consistency"] = self.consistency.value
|
|
547
|
-
|
|
547
|
+
# Store expanded bone names, not raw refKeys patterns.
|
|
548
|
+
# refKeys may contain fnmatch wildcards (e.g. "delivery_time_*" matching
|
|
549
|
+
# "delivery_time_min", "delivery_time_max", "delivery_time_range").
|
|
550
|
+
# update_relations filters viur-relations via Datastore IN-query with the
|
|
551
|
+
# literal changed bone name — wildcard patterns would never match there.
|
|
552
|
+
entity["viur_foreign_keys"] = list(self._ref_keys)
|
|
548
553
|
entity["viurTags"] = skel.dbEntity.get("viurTags") if skel.dbEntity else None
|
|
549
554
|
|
|
550
555
|
db.put(entity)
|
|
@@ -640,6 +645,10 @@ class RelationalBone(BaseBone):
|
|
|
640
645
|
dest_key = value
|
|
641
646
|
value = {}
|
|
642
647
|
|
|
648
|
+
if not isinstance(dest_key, db.KeyType):
|
|
649
|
+
errors.append(ReadFromClientError(ReadFromClientErrorSeverity.Invalid))
|
|
650
|
+
return self.getEmptyValue(), errors
|
|
651
|
+
|
|
643
652
|
if self.using:
|
|
644
653
|
rel = self.using()
|
|
645
654
|
if not rel.fromClient(value):
|
|
@@ -1066,8 +1075,12 @@ class RelationalBone(BaseBone):
|
|
|
1066
1075
|
# Reset the dbEntity for a clean rewrite
|
|
1067
1076
|
value["dest"].dbEntity = None
|
|
1068
1077
|
|
|
1069
|
-
# Copy over the refKey values
|
|
1070
|
-
|
|
1078
|
+
# Copy over the refKey values using expanded bone names (_ref_keys),
|
|
1079
|
+
# not raw refKeys patterns. refKeys may contain fnmatch wildcards
|
|
1080
|
+
# (e.g. "delivery_time_*" → "delivery_time_min", "delivery_time_max",
|
|
1081
|
+
# "delivery_time_range"). Iterating raw patterns would attempt
|
|
1082
|
+
# target_skel["delivery_time_*"] which doesn't exist → copies None.
|
|
1083
|
+
for key in self._ref_keys:
|
|
1071
1084
|
value["dest"][key] = target_skel[key]
|
|
1072
1085
|
# logging.debug(f"Refreshed {key=} to {value["dest"][key]!r} ({str(value["dest"][key])!r})")
|
|
1073
1086
|
|
|
@@ -1258,24 +1271,32 @@ class RelationalBone(BaseBone):
|
|
|
1258
1271
|
|
|
1259
1272
|
return result
|
|
1260
1273
|
|
|
1261
|
-
def getUniquePropertyIndexValues(self,
|
|
1274
|
+
def getUniquePropertyIndexValues(self, skel: "SkeletonInstance", name: str) -> list[str]:
|
|
1262
1275
|
"""
|
|
1263
1276
|
Generates unique property index values for the RelationalBone based on the referenced keys.
|
|
1264
1277
|
Can be overridden if different behavior is required (e.g., examining values from `prop:usingSkel`).
|
|
1265
1278
|
|
|
1266
|
-
:param
|
|
1279
|
+
:param skel: The skeleton instance.
|
|
1267
1280
|
:param str name: The name of the bone for which to generate unique property index values.
|
|
1268
1281
|
|
|
1269
1282
|
:return: A list containing the unique property index values for the specified bone.
|
|
1270
1283
|
:rtype: List[str]
|
|
1271
1284
|
"""
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1285
|
+
values = []
|
|
1286
|
+
|
|
1287
|
+
for _, _, v in self.iter_bone_value(skel, name):
|
|
1288
|
+
if not v:
|
|
1289
|
+
continue
|
|
1290
|
+
|
|
1291
|
+
if self.using and (rel_skel := v.get("rel")):
|
|
1292
|
+
values.append(json.dumps(
|
|
1293
|
+
{"key": str(v["dest"]["key"]), "rel": rel_skel.dump()},
|
|
1294
|
+
sort_keys=True, default=str,
|
|
1295
|
+
))
|
|
1296
|
+
else:
|
|
1297
|
+
values.append(v["dest"]["key"])
|
|
1298
|
+
|
|
1299
|
+
return self._hashValueForUniquePropertyIndex(values) if values else []
|
|
1279
1300
|
|
|
1280
1301
|
def structure(self) -> dict:
|
|
1281
1302
|
return super().structure() | {
|
|
@@ -1287,7 +1308,7 @@ class RelationalBone(BaseBone):
|
|
|
1287
1308
|
}
|
|
1288
1309
|
|
|
1289
1310
|
def _atomic_dump(self, value: dict[str, "SkeletonInstance"]) -> dict | None:
|
|
1290
|
-
if isinstance(value, dict):
|
|
1311
|
+
if value and isinstance(value, dict): # can be an empty dict due RelationalConsistency.SetNull
|
|
1291
1312
|
return {
|
|
1292
1313
|
"dest": value["dest"].dump(),
|
|
1293
1314
|
"rel": value["rel"].dump() if value["rel"] else None,
|
|
@@ -292,19 +292,15 @@ class StringBone(RawBone):
|
|
|
292
292
|
:param skel: The skeleton instance.
|
|
293
293
|
:param name: The name of the property.
|
|
294
294
|
:return: A list of unique index values for the property.
|
|
295
|
-
:raises NotImplementedError: If the StringBone has languages and the implementation
|
|
296
|
-
for this case is not yet defined.
|
|
297
295
|
"""
|
|
298
|
-
if self.
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
if not self.caseSensitive:
|
|
297
|
+
values = [
|
|
298
|
+
value.lower() if isinstance(value, str) else value
|
|
299
|
+
for _, _, value in self.iter_bone_value(skel, name)
|
|
300
|
+
if value is not None
|
|
301
|
+
]
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
if self.multiple:
|
|
304
|
-
value = [v.lower() for v in value]
|
|
305
|
-
else:
|
|
306
|
-
value = value.lower()
|
|
307
|
-
return self._hashValueForUniquePropertyIndex(value)
|
|
303
|
+
return self._hashValueForUniquePropertyIndex(values) if values else []
|
|
308
304
|
|
|
309
305
|
return super().getUniquePropertyIndexValues(skel, name)
|
|
310
306
|
|
|
@@ -438,25 +438,6 @@ class TextBone(RawBone):
|
|
|
438
438
|
elif not self.languages and isinstance(val, str):
|
|
439
439
|
skel[boneName] = self.singleValueFromClient(val, skel, boneName, None)[0]
|
|
440
440
|
|
|
441
|
-
def getUniquePropertyIndexValues(self, valuesCache: dict, name: str) -> list[str]:
|
|
442
|
-
"""
|
|
443
|
-
Retrieves the unique property index values for the TextBone.
|
|
444
|
-
|
|
445
|
-
If the TextBone supports multiple languages, this method raises a NotImplementedError, as it's unclear
|
|
446
|
-
whether each language should be kept distinct or not. Otherwise, it calls the superclass's
|
|
447
|
-
getUniquePropertyIndexValues method to retrieve the unique property index values.
|
|
448
|
-
|
|
449
|
-
:param valuesCache: A dictionary containing the cached values for the TextBone.
|
|
450
|
-
:param name: The name of the TextBone.
|
|
451
|
-
:return: A list of unique property index values for the TextBone.
|
|
452
|
-
:raises NotImplementedError: If the TextBone supports multiple languages.
|
|
453
|
-
"""
|
|
454
|
-
if self.languages:
|
|
455
|
-
# Not yet implemented as it's unclear if we should keep each language distinct or not
|
|
456
|
-
raise NotImplementedError()
|
|
457
|
-
|
|
458
|
-
return super().getUniquePropertyIndexValues(valuesCache, name)
|
|
459
|
-
|
|
460
441
|
def structure(self) -> dict:
|
|
461
442
|
return super().structure() | {
|
|
462
443
|
"valid_html": self.validHtml,
|
|
@@ -553,6 +553,7 @@ def flushCache(prefix: str = None, key: db.Key | None = None, kind: str | None
|
|
|
553
553
|
"""
|
|
554
554
|
if prefix is None and key is None and kind is None:
|
|
555
555
|
prefix = "/*"
|
|
556
|
+
|
|
556
557
|
if prefix is not None:
|
|
557
558
|
items = db.Query(CACHE_KINDNAME).filter("path =", prefix.rstrip("*")).iter()
|
|
558
559
|
for item in items:
|
|
@@ -565,20 +566,20 @@ def flushCache(prefix: str = None, key: db.Key | None = None, kind: str | None
|
|
|
565
566
|
for item in items:
|
|
566
567
|
db.delete(item)
|
|
567
568
|
logging.debug(f"Flushing cache succeeded. Everything matching {prefix=} is gone.")
|
|
569
|
+
|
|
568
570
|
if key is not None:
|
|
569
571
|
items = db.Query(CACHE_KINDNAME).filter("accessedEntries =", key).iter()
|
|
572
|
+
|
|
570
573
|
for item in items:
|
|
571
574
|
logging.info(f"""Deleted cache entry {item["path"]!r}""")
|
|
572
575
|
db.delete(item.key)
|
|
573
|
-
|
|
576
|
+
|
|
577
|
+
if kind is None and not isinstance(key, db.Key):
|
|
574
578
|
key = db.Key.from_legacy_urlsafe(key) # hopefully is a string
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
logging.info(f"""Deleted cache entry {item["path"]!r}""")
|
|
578
|
-
db.delete(item.key)
|
|
579
|
+
kind = key.kind
|
|
580
|
+
|
|
579
581
|
if kind is not None:
|
|
580
582
|
items = db.Query(CACHE_KINDNAME).filter("accessedEntries =", kind).iter()
|
|
581
583
|
for item in items:
|
|
582
584
|
logging.info(f"""Deleted cache entry {item["path"]!r}""")
|
|
583
585
|
db.delete(item.key)
|
|
584
|
-
|
|
@@ -580,7 +580,10 @@ class File(Tree):
|
|
|
580
580
|
|
|
581
581
|
@classmethod
|
|
582
582
|
def hmac_verify(cls, data: t.Any, signature: str) -> bool:
|
|
583
|
-
|
|
583
|
+
try:
|
|
584
|
+
return hmac.compare_digest(cls.hmac_sign(data.encode("ASCII")), signature)
|
|
585
|
+
except (TypeError, UnicodeEncodeError):
|
|
586
|
+
return False
|
|
584
587
|
|
|
585
588
|
@classmethod
|
|
586
589
|
def create_internal_serving_url(
|
|
@@ -654,8 +657,7 @@ class File(Tree):
|
|
|
654
657
|
if isinstance(expires, int):
|
|
655
658
|
expires = datetime.timedelta(minutes=expires)
|
|
656
659
|
|
|
657
|
-
|
|
658
|
-
filename = filename.replace("(", "(").replace(")", ")").replace("=", "=")
|
|
660
|
+
filename = html.unescape(filename)
|
|
659
661
|
filepath = f"""{dlkey}/{"derived" if derived else "source"}/{filename}"""
|
|
660
662
|
|
|
661
663
|
if download_filename:
|
|
@@ -107,19 +107,21 @@ def normalizeKey(key: t.Union[None, db.Key]) -> t.Union[None, db.Key]:
|
|
|
107
107
|
def get_base_url() -> str:
|
|
108
108
|
"""
|
|
109
109
|
Retrieve current request's base URL with protocol.
|
|
110
|
+
The function enforces use of https-protocol on non-localhost hostnames.
|
|
110
111
|
|
|
111
112
|
:returns: Returns the hostname, including the currently used protocol, e.g: https://www.example.com
|
|
112
113
|
:rtype: str
|
|
113
114
|
"""
|
|
114
|
-
|
|
115
|
-
url = url[:url.find("/", url.find("://") + 5)] # cut out base-url
|
|
115
|
+
base = urllib.parse.urlparse(current.request.get().request.url).netloc # retrieve URL of request
|
|
116
116
|
|
|
117
117
|
# Always enforce https!
|
|
118
|
-
if
|
|
119
|
-
|
|
118
|
+
if any(base.startswith(i) for i in ("localhost", "127.0.0.1", "[::1]", "0.0.0.0")):
|
|
119
|
+
base = f"http://{base}"
|
|
120
|
+
else:
|
|
121
|
+
base = f"https://{base}"
|
|
120
122
|
|
|
121
123
|
# Replace non-SSL-ready-"appspot.com"-URLs with their SSL-ready counterpart
|
|
122
|
-
return
|
|
124
|
+
return base.replace(f".{conf.instance.project_id}.", f"-dot-{conf.instance.project_id}.")
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
def ensure_iterable(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# This will mark it as a pre-release as well on PyPI.
|
|
4
4
|
# See CONTRIBUTING.md for further information.
|
|
5
5
|
|
|
6
|
-
__version__ = "3.9.0.
|
|
6
|
+
__version__ = "3.9.0.dev3"
|
|
7
7
|
|
|
8
8
|
assert __version__.count(".") >= 2 and "".join(__version__.split(".", 3)[:3]).isdigit(), \
|
|
9
9
|
"Semantic __version__ expected!"
|
|
@@ -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.dev3
|
|
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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import timedelta as td
|
|
2
|
+
from unittest import mock
|
|
2
3
|
|
|
3
4
|
from abstract import ViURTestCase
|
|
4
5
|
|
|
@@ -17,6 +18,16 @@ class TestUtils(ViURTestCase):
|
|
|
17
18
|
self.assertEqual(utils.string.unescape("Hello'World's"), "Hello'World's")
|
|
18
19
|
self.assertEqual(utils.string.unescape(E), S.replace("\n", " "))
|
|
19
20
|
|
|
21
|
+
def test_string_unescape_filename_entities(self):
|
|
22
|
+
"""unescape() must handle both 2- and 3-digit numeric HTML entities used in filenames."""
|
|
23
|
+
from viur.core import utils
|
|
24
|
+
# short-form: ( ) =
|
|
25
|
+
self.assertEqual(utils.string.unescape("file(1)=x.pdf"), "file(1)=x.pdf")
|
|
26
|
+
# long-form (leading zero): ( ) =
|
|
27
|
+
self.assertEqual(utils.string.unescape("file(1)=x.pdf"), "file(1)=x.pdf")
|
|
28
|
+
# mixed
|
|
29
|
+
self.assertEqual(utils.string.unescape("(test)=val"), "(test)=val")
|
|
30
|
+
|
|
20
31
|
def test_string_escape(self):
|
|
21
32
|
from viur.core import utils
|
|
22
33
|
self.assertEqual("None", utils.string.escape(None))
|
|
@@ -76,3 +87,70 @@ class TestUtils(ViURTestCase):
|
|
|
76
87
|
self.assertEqual(td(seconds=60), utils.parse.timedelta("60"))
|
|
77
88
|
self.assertEqual(td(seconds=60), utils.parse.timedelta("60.0"))
|
|
78
89
|
self.assertNotEqual(td(seconds=0), utils.parse.timedelta(60.0))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _make_request(url: str):
|
|
93
|
+
"""Return a mock mimicking current.request.get() with .request.url set."""
|
|
94
|
+
req = mock.Mock()
|
|
95
|
+
req.request.url = url
|
|
96
|
+
ctx_var = mock.Mock()
|
|
97
|
+
ctx_var.get.return_value = req
|
|
98
|
+
return ctx_var
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestGetBaseUrl(ViURTestCase):
|
|
102
|
+
|
|
103
|
+
def _call(self, url: str, project_id: str = "myproject") -> str:
|
|
104
|
+
from viur.core import utils
|
|
105
|
+
from viur.core import current
|
|
106
|
+
from viur.core.config import conf
|
|
107
|
+
|
|
108
|
+
with mock.patch.object(current, "request", _make_request(url)), \
|
|
109
|
+
mock.patch.object(conf.instance, "project_id", project_id):
|
|
110
|
+
return utils.get_base_url()
|
|
111
|
+
|
|
112
|
+
# --- localhost variants → http ---
|
|
113
|
+
|
|
114
|
+
def test_localhost_plain(self):
|
|
115
|
+
self.assertEqual(self._call("http://localhost:8080/foo"), "http://localhost:8080")
|
|
116
|
+
|
|
117
|
+
def test_localhost_no_port(self):
|
|
118
|
+
self.assertEqual(self._call("http://localhost/"), "http://localhost")
|
|
119
|
+
|
|
120
|
+
def test_127_0_0_1(self):
|
|
121
|
+
self.assertEqual(self._call("http://127.0.0.1:8080/bar"), "http://127.0.0.1:8080")
|
|
122
|
+
|
|
123
|
+
def test_ipv4_all_interfaces(self):
|
|
124
|
+
self.assertEqual(self._call("http://0.0.0.0:8080/"), "http://0.0.0.0:8080")
|
|
125
|
+
|
|
126
|
+
def test_ipv6_loopback(self):
|
|
127
|
+
self.assertEqual(self._call("http://[::1]:8080/"), "http://[::1]:8080")
|
|
128
|
+
|
|
129
|
+
# --- non-localhost → https ---
|
|
130
|
+
|
|
131
|
+
def test_plain_domain(self):
|
|
132
|
+
self.assertEqual(self._call("http://www.example.com/path"), "https://www.example.com")
|
|
133
|
+
|
|
134
|
+
def test_already_https(self):
|
|
135
|
+
self.assertEqual(self._call("https://www.example.com/path"), "https://www.example.com")
|
|
136
|
+
|
|
137
|
+
def test_subdomain(self):
|
|
138
|
+
self.assertEqual(self._call("https://api.example.com/v1/endpoint"), "https://api.example.com")
|
|
139
|
+
|
|
140
|
+
# --- appspot.com dot-replacement ---
|
|
141
|
+
|
|
142
|
+
def test_appspot_dot_replaced(self):
|
|
143
|
+
# .myproject. in hostname → -dot-myproject.
|
|
144
|
+
result = self._call(
|
|
145
|
+
"https://default.myproject.appspot.com/",
|
|
146
|
+
project_id="myproject",
|
|
147
|
+
)
|
|
148
|
+
self.assertEqual(result, "https://default-dot-myproject.appspot.com")
|
|
149
|
+
|
|
150
|
+
def test_appspot_no_replacement_without_project_id_match(self):
|
|
151
|
+
# project_id does not appear in the host → no replacement
|
|
152
|
+
result = self._call(
|
|
153
|
+
"https://www.example.com/",
|
|
154
|
+
project_id="myproject",
|
|
155
|
+
)
|
|
156
|
+
self.assertEqual(result, "https://www.example.com")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
{viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/template/vi_user_google_login.html
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
|