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.
Files changed (122) hide show
  1. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/PKG-INFO +1 -1
  2. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/base.py +22 -17
  3. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/boolean.py +7 -2
  4. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/record.py +10 -5
  5. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/relational.py +34 -13
  6. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/string.py +7 -11
  7. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/text.py +0 -19
  8. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/cache.py +7 -6
  9. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/file.py +5 -3
  10. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/__init__.py +7 -5
  11. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/version.py +1 -1
  12. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/PKG-INFO +1 -1
  13. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/tests/test_utils.py +78 -0
  14. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/LICENSE +0 -0
  15. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/README.md +0 -0
  16. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/pyproject.toml +0 -0
  17. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/setup.cfg +0 -0
  18. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/__init__.py +0 -0
  19. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/__init__.py +0 -0
  20. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/captcha.py +0 -0
  21. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/color.py +0 -0
  22. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/credential.py +0 -0
  23. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/date.py +0 -0
  24. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/email.py +0 -0
  25. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/file.py +0 -0
  26. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/image.py +0 -0
  27. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/json.py +0 -0
  28. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/key.py +0 -0
  29. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/numeric.py +0 -0
  30. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/password.py +0 -0
  31. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/phone.py +0 -0
  32. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/randomslice.py +0 -0
  33. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/raw.py +0 -0
  34. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/select.py +0 -0
  35. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/selectcountry.py +0 -0
  36. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/sortindex.py +0 -0
  37. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/spam.py +0 -0
  38. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/spatial.py +0 -0
  39. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/treeleaf.py +0 -0
  40. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/treenode.py +0 -0
  41. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/uid.py +0 -0
  42. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/uri.py +0 -0
  43. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/bones/user.py +0 -0
  44. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/config.py +0 -0
  45. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/current.py +0 -0
  46. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/__init__.py +0 -0
  47. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/cache.py +0 -0
  48. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/config.py +0 -0
  49. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/overrides.py +0 -0
  50. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/query.py +0 -0
  51. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/transport.py +0 -0
  52. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/types.py +0 -0
  53. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/db/utils.py +0 -0
  54. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/decorators.py +0 -0
  55. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/email.py +0 -0
  56. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/errors.py +0 -0
  57. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/i18n.py +0 -0
  58. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/__init__.py +0 -0
  59. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/de.py +0 -0
  60. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/languages/en.py +0 -0
  61. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/logging.py +0 -0
  62. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/module.py +0 -0
  63. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/__init__.py +0 -0
  64. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/formmailer.py +0 -0
  65. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/history.py +0 -0
  66. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/moduleconf.py +0 -0
  67. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/page.py +0 -0
  68. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/script.py +0 -0
  69. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/site.py +0 -0
  70. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/translation.py +0 -0
  71. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/modules/user.py +0 -0
  72. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/pagination.py +0 -0
  73. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/__init__.py +0 -0
  74. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/instanced_module.py +0 -0
  75. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/list.py +0 -0
  76. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/singleton.py +0 -0
  77. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/skelmodule.py +0 -0
  78. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/prototypes/tree.py +0 -0
  79. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/ratelimit.py +0 -0
  80. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/__init__.py +0 -0
  81. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/abstract.py +0 -0
  82. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/__init__.py +0 -0
  83. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/default.py +0 -0
  84. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/__init__.py +0 -0
  85. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/date.py +0 -0
  86. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/debug.py +0 -0
  87. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/regex.py +0 -0
  88. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/session.py +0 -0
  89. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/strings.py +0 -0
  90. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/tests.py +0 -0
  91. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/env/viur.py +0 -0
  92. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/html/utils.py +0 -0
  93. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/json/__init__.py +0 -0
  94. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/json/default.py +0 -0
  95. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/render/vi/__init__.py +0 -0
  96. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/request.py +0 -0
  97. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/scripts/viur_migrate.py +0 -0
  98. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/secret.py +0 -0
  99. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/securityheaders.py +0 -0
  100. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/securitykey.py +0 -0
  101. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/session.py +0 -0
  102. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/__init__.py +0 -0
  103. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/adapter.py +0 -0
  104. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/instance.py +0 -0
  105. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/meta.py +0 -0
  106. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/relskel.py +0 -0
  107. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/skeleton.py +0 -0
  108. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/tasks.py +0 -0
  109. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/skeleton/utils.py +0 -0
  110. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/tasks.py +0 -0
  111. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/template/error.html +0 -0
  112. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/template/vi_user_google_login.html +0 -0
  113. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/json.py +0 -0
  114. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/parse.py +0 -0
  115. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur/core/utils/string.py +0 -0
  116. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/SOURCES.txt +0 -0
  117. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/dependency_links.txt +0 -0
  118. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/entry_points.txt +0 -0
  119. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/requires.txt +0 -0
  120. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/src/viur_core.egg-info/top_level.txt +0 -0
  121. {viur_core-3.9.0.dev1 → viur_core-3.9.0.dev3}/tests/test_config.py +0 -0
  122. {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.dev1
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 hashValue(value: str | int | float | db.Key) -> str:
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"{hashValue(key.kind)}-{hashValue(key.id_or_name)}-<{keyHash(key.parent)}>"
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 [] # We are zero/empty string and these should not be locked
1317
- if not self.multiple and not isinstance(value, list):
1318
- return [hashValue(value)]
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
- tmpList = [hashValue(x) for x in value]
1324
+
1325
+ values = [hash_value(val) for val in value]
1326
+
1323
1327
  if self.unique.method == UniqueLockMethod.SameValue:
1324
- # We should lock each entry individually; lock each value
1325
- return tmpList
1328
+ # Lock each entry individually
1329
+ return values
1330
+
1326
1331
  elif self.unique.method == UniqueLockMethod.SameSet:
1327
- # We should ignore the sort-order; so simply sort that List
1328
- tmpList.sort()
1329
- # Lock the value for that specific list
1330
- return [hashValue(", ".join(tmpList))]
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
- val = skel[name]
1347
- if val is None:
1348
- return []
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
- return utils.parse.bool(value, conf.bone_boolean_str2true), None
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, valuesCache: dict, name: str) -> list[str]:
218
+ def getUniquePropertyIndexValues(self, skel: "SkeletonInstance", name: str) -> list[str]:
219
219
  """
220
- This method is intentionally not implemented as it's not possible to determine how to derive
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
- raise NotImplementedError()
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
- entity["viur_foreign_keys"] = list(self.refKeys)
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
- for key in self.refKeys:
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, valuesCache: dict, name: str) -> list[str]:
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 dict valuesCache: The cache containing the current values of the bone.
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
- value = valuesCache.get(name)
1273
- if not value: # We don't have a value to lock
1274
- return []
1275
- if isinstance(value, dict):
1276
- return self._hashValueForUniquePropertyIndex(value["dest"]["key"])
1277
- elif isinstance(value, list):
1278
- return self._hashValueForUniquePropertyIndex([entry["dest"]["key"] for entry in value if entry])
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.languages:
299
- # Not yet implemented as it's unclear if we should keep each language distinct or not
300
- raise NotImplementedError()
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
- if not self.caseSensitive and (value := skel[name]) is not None:
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
- if not isinstance(key, db.Key):
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
- items = db.Query(CACHE_KINDNAME).filter("accessedEntries =", key.kind).iter()
576
- for item in items:
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
- return hmac.compare_digest(cls.hmac_sign(data.encode("ASCII")), signature)
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
- # Undo escaping on ()= performed on fileNames
658
- filename = filename.replace("&#040;", "(").replace("&#041;", ")").replace("&#061;", "=")
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
- url = current.request.get().request.url # retrireve URL by request
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 not any(url.startswith(f"http://{i}") for i in ("localhost", "127.0.0.1")):
119
- url = "https://" + url[7:]
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 url.replace(f".{conf.instance.project_id}.", f"-dot-{conf.instance.project_id}.")
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.dev1"
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.dev1
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&#039;World&#39;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: &#40; &#41; &#61;
25
+ self.assertEqual(utils.string.unescape("file&#40;1&#41;&#61;x.pdf"), "file(1)=x.pdf")
26
+ # long-form (leading zero): &#040; &#041; &#061;
27
+ self.assertEqual(utils.string.unescape("file&#040;1&#041;&#061;x.pdf"), "file(1)=x.pdf")
28
+ # mixed
29
+ self.assertEqual(utils.string.unescape("&#040;test&#41;&#061;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