viur-core 3.8.0.dev6__tar.gz → 3.8.0.dev7__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 (124) hide show
  1. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/PKG-INFO +1 -1
  2. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/file.py +6 -2
  3. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/utils.py +0 -1
  4. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/i18n.py +1 -1
  5. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/file.py +3 -11
  6. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/translation.py +9 -11
  7. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/request.py +5 -2
  8. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/instance.py +1 -0
  9. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/skeleton.py +31 -19
  10. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/tasks.py +42 -27
  11. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/utils/__init__.py +48 -4
  12. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/utils/string.py +23 -0
  13. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/version.py +1 -1
  14. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/PKG-INFO +1 -1
  15. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/LICENSE +0 -0
  16. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/README.md +0 -0
  17. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/pyproject.toml +0 -0
  18. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/setup.cfg +0 -0
  19. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/__init__.py +0 -0
  20. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/__init__.py +0 -0
  21. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/base.py +0 -0
  22. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/boolean.py +0 -0
  23. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/captcha.py +0 -0
  24. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/color.py +0 -0
  25. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/credential.py +0 -0
  26. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/date.py +0 -0
  27. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/email.py +0 -0
  28. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/image.py +0 -0
  29. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/json.py +0 -0
  30. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/key.py +0 -0
  31. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/numeric.py +0 -0
  32. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/password.py +0 -0
  33. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/phone.py +0 -0
  34. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/randomslice.py +0 -0
  35. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/raw.py +0 -0
  36. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/record.py +0 -0
  37. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/relational.py +0 -0
  38. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/select.py +0 -0
  39. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/selectcountry.py +0 -0
  40. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/sortindex.py +0 -0
  41. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/spam.py +0 -0
  42. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/spatial.py +0 -0
  43. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/string.py +0 -0
  44. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/text.py +0 -0
  45. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/treeleaf.py +0 -0
  46. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/treenode.py +0 -0
  47. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/uid.py +0 -0
  48. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/uri.py +0 -0
  49. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/bones/user.py +0 -0
  50. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/cache.py +0 -0
  51. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/config.py +0 -0
  52. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/current.py +0 -0
  53. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/__init__.py +0 -0
  54. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/cache.py +0 -0
  55. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/config.py +0 -0
  56. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/overrides.py +0 -0
  57. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/query.py +0 -0
  58. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/transport.py +0 -0
  59. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/db/types.py +0 -0
  60. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/decorators.py +0 -0
  61. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/email.py +0 -0
  62. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/errors.py +0 -0
  63. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/languages/__init__.py +0 -0
  64. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/languages/de.py +0 -0
  65. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/languages/en.py +0 -0
  66. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/logging.py +0 -0
  67. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/module.py +0 -0
  68. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/__init__.py +0 -0
  69. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/formmailer.py +0 -0
  70. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/history.py +0 -0
  71. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/moduleconf.py +0 -0
  72. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/page.py +0 -0
  73. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/script.py +0 -0
  74. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/site.py +0 -0
  75. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/modules/user.py +0 -0
  76. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/pagination.py +0 -0
  77. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/__init__.py +0 -0
  78. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/instanced_module.py +0 -0
  79. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/list.py +0 -0
  80. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/singleton.py +0 -0
  81. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/skelmodule.py +0 -0
  82. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/prototypes/tree.py +0 -0
  83. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/ratelimit.py +0 -0
  84. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/__init__.py +0 -0
  85. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/abstract.py +0 -0
  86. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/__init__.py +0 -0
  87. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/default.py +0 -0
  88. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/__init__.py +0 -0
  89. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/date.py +0 -0
  90. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/debug.py +0 -0
  91. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/regex.py +0 -0
  92. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/session.py +0 -0
  93. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/strings.py +0 -0
  94. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/tests.py +0 -0
  95. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/env/viur.py +0 -0
  96. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/user.py +0 -0
  97. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/html/utils.py +0 -0
  98. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/json/__init__.py +0 -0
  99. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/json/default.py +0 -0
  100. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/json/user.py +0 -0
  101. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/vi/__init__.py +0 -0
  102. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/render/vi/user.py +0 -0
  103. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/scripts/viur_migrate.py +0 -0
  104. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/secret.py +0 -0
  105. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/securityheaders.py +0 -0
  106. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/securitykey.py +0 -0
  107. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/session.py +0 -0
  108. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/__init__.py +0 -0
  109. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/adapter.py +0 -0
  110. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/meta.py +0 -0
  111. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/relskel.py +0 -0
  112. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/skeleton/utils.py +0 -0
  113. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/tasks.py +0 -0
  114. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/template/error.html +0 -0
  115. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/template/vi_user_google_login.html +0 -0
  116. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/utils/json.py +0 -0
  117. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur/core/utils/parse.py +0 -0
  118. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/SOURCES.txt +0 -0
  119. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/dependency_links.txt +0 -0
  120. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/entry_points.txt +0 -0
  121. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/requires.txt +0 -0
  122. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/src/viur_core.egg-info/top_level.txt +0 -0
  123. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/tests/test_config.py +0 -0
  124. {viur_core-3.8.0.dev6 → viur_core-3.8.0.dev7}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viur-core
3
- Version: 3.8.0.dev6
3
+ Version: 3.8.0.dev7
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>
@@ -35,7 +35,8 @@ def ensureDerived(key: db.Key, srcKey, deriveMap: dict[str, t.Any], refreshKey:
35
35
  the updated results are written back to the database and the updateRelations function is called
36
36
  to ensure proper relations are maintained.
37
37
  """
38
- from viur.core.skeleton import skeletonByKind, updateRelations
38
+ from viur.core.skeleton.utils import skeletonByKind
39
+ from viur.core.skeleton.tasks import updateRelations
39
40
  deriveFuncMap = conf.file_derivations
40
41
  skel = skeletonByKind("file")()
41
42
  if not skel.read(key):
@@ -226,7 +227,10 @@ class FileBone(TreeLeafBone):
226
227
  the derived files directly.
227
228
  """
228
229
  super().postSavedHandler(skel, boneName, key)
229
- if current.request.get().is_deferred and current.request_data.get().get("__update_relations_bone") == "derived":
230
+ if (
231
+ current.request.get().is_deferred
232
+ and "derived" in current.request_data.get().get("__update_relations_bones")
233
+ ):
230
234
  return
231
235
  from viur.core.skeleton import RelSkel, Skeleton
232
236
 
@@ -88,7 +88,6 @@ def key_helper(
88
88
  decoded_key = None
89
89
 
90
90
  # If it did decode, recall keyHelper with Key object
91
- print(f"decoded key: {decoded_key!r}")
92
91
  if decoded_key:
93
92
  return key_helper(
94
93
  decoded_key,
@@ -501,7 +501,7 @@ def add_missing_translation(
501
501
  key = key.lower()
502
502
 
503
503
  # Check if key already exists
504
- # if db.Get(db.Key(KINDNAME, key)): # FIXME ViUR4 should only use named keys
504
+ # if db.get(db.Key(KINDNAME, key)): # FIXME ViUR4 should only use named keys
505
505
  entity = db.Query(KINDNAME).filter("name =", key).getEntry()
506
506
  if entity is not None:
507
507
  # Ensure it doesn't exist to avoid datastore conflicts
@@ -198,7 +198,7 @@ def cloudfunction_thumbnailer(fileSkel, existingFiles, params):
198
198
  authRequest = google.auth.transport.requests.Request()
199
199
  expiresAt = datetime.datetime.now() + datetime.timedelta(seconds=60)
200
200
  signing_credentials = google.auth.compute_engine.IDTokenCredentials(authRequest, "")
201
- content_disposition = f"""filename={fileSkel["name"]}"""
201
+ content_disposition = utils.build_content_disposition_header(fileSkel["name"])
202
202
  signedUrl = blob.generate_signed_url(
203
203
  expiresAt,
204
204
  credentials=signing_credentials,
@@ -1029,12 +1029,7 @@ class File(Tree):
1029
1029
  if not filename:
1030
1030
  filename = download_filename or urlquote(blob.name.rsplit("/", 1)[-1])
1031
1031
 
1032
- content_disposition = "; ".join(
1033
- item for item in (
1034
- "attachment" if download else None,
1035
- f"filename={filename}" if filename else None,
1036
- ) if item
1037
- )
1032
+ content_disposition = utils.build_content_disposition_header(filename, attachment=download)
1038
1033
 
1039
1034
  if isinstance(_CREDENTIALS, ServiceAccountCredentials):
1040
1035
  expiresAt = datetime.datetime.now() + datetime.timedelta(seconds=60)
@@ -1152,10 +1147,7 @@ class File(Tree):
1152
1147
  response = current.request.get().response
1153
1148
  response.headers["Content-Type"] = f"image/{file_fmt}"
1154
1149
  response.headers["Cache-Control"] = "public, max-age=604800" # 7 Days
1155
- if download:
1156
- response.headers["Content-Disposition"] = f"attachment; filename={filename}"
1157
- else:
1158
- response.headers["Content-Disposition"] = f"filename={filename}"
1150
+ response.headers["Content-Disposition"] = utils.build_content_disposition_header(filename, attachment=download)
1159
1151
 
1160
1152
  answ = requests.get(url, timeout=20)
1161
1153
  if not answ.ok:
@@ -255,14 +255,17 @@ class Translation(List):
255
255
  def dump(
256
256
  self,
257
257
  *,
258
- pattern: list[str] = [],
259
- language: list[str] = [],
260
- ) -> dict[str, str] | dict[str, dict[str, str]]:
258
+ pattern: list[str] | None = None,
259
+ language: list[str] | None = None,
260
+ ) -> dict[str, dict[str, str]]:
261
261
  """
262
262
  Dumps translations as JSON.
263
263
 
264
- :param pattern: Required, provide fnmatch-style tramslaton key filter patterns of the translations wanted.
264
+ :param pattern: Optional, provide fnmatch-style translation key filter patterns of the translations wanted.
265
265
  :param language: Allows to request a specific language.
266
+ By default, the language of the current request is used.
267
+
268
+ :return: A dictionary with translations as JSON. Structure: ``{language: {key: value, ...}, ...}``
266
269
 
267
270
  Example calls:
268
271
 
@@ -276,11 +279,6 @@ class Translation(List):
276
279
 
277
280
  current.request.get().response.headers["Content-Type"] = "application/json"
278
281
 
279
- # The pattern may not be a matcher for all!
280
- for pat in pattern:
281
- if not pat.strip("*?."):
282
- raise errors.BadRequest("Pattern is too generic.")
283
-
284
282
  if (
285
283
  not (conf.debug.disable_cache and current.request.get().disableCache)
286
284
  and any(os.getenv("HTTP_HOST", "") in dlm for dlm in conf.i18n.domain_language_mapping)
@@ -294,12 +292,12 @@ class Translation(List):
294
292
  else:
295
293
  language = [current.language.get()]
296
294
 
297
- return json.dumps({
295
+ return json.dumps({ # type: ignore
298
296
  lang: {
299
297
  name: str(translate(name, force_lang=lang))
300
298
  for name, values in systemTranslations.items()
301
299
  if (conf.i18n.dump_can_view(name) or values.get("_public_"))
302
- and any(fnmatch.fnmatch(name, pat) for pat in pattern)
300
+ and (not pattern or any(fnmatch.fnmatch(name, pat) for pat in pattern))
303
301
  }
304
302
  for lang in language
305
303
  })
@@ -14,11 +14,11 @@ import re
14
14
  import time
15
15
  import traceback
16
16
  import typing as t
17
+ import unicodedata
17
18
  from abc import ABC, abstractmethod
18
19
  from urllib import parse
19
- from urllib.parse import unquote, urljoin, urlparse
20
+ from urllib.parse import quote, unquote, urljoin, urlparse
20
21
 
21
- import unicodedata
22
22
  import webob
23
23
 
24
24
  from viur.core import current, db, errors, session, utils
@@ -367,6 +367,9 @@ class Router:
367
367
  raise
368
368
  self.response.status = f"{e.status} {e.name}"
369
369
  url = e.url
370
+ url = unquote(url) # decode first
371
+ # safe = https://url.spec.whatwg.org/#url-path-segment-string
372
+ url = quote(url, encoding="utf-8", safe="!$&'()*+,-./:;=?@_~") # re-encode all in utf-8
370
373
  if url.startswith(('.', '/')):
371
374
  url = str(urljoin(self.request.url, url))
372
375
  self.response.headers['Location'] = url
@@ -8,6 +8,7 @@ import warnings
8
8
  from functools import partial
9
9
  from ..bones.base import BaseBone
10
10
  from .skeleton import Skeleton
11
+ from viur.core import db
11
12
 
12
13
 
13
14
  class SkeletonInstance:
@@ -10,13 +10,25 @@ from deprecated.sphinx import deprecated
10
10
  from viur.core import conf, db, errors, utils
11
11
 
12
12
  from .meta import BaseSkeleton, MetaSkel, _UNDEFINED_KINDNAME
13
- from .tasks import updateRelations
14
- from ..bones.base import Compute, ComputeInterval, ComputeMethod, ReadFromClientException
13
+ from .tasks import updateRelations, processRemovedRelations
14
+ from .utils import skeletonByKind
15
+ from ..bones.base import (
16
+ Compute,
17
+ ComputeInterval,
18
+ ComputeMethod,
19
+ ReadFromClientException,
20
+ ReadFromClientError,
21
+ ReadFromClientErrorSeverity
22
+ )
15
23
  from ..bones.relational import RelationalConsistency
16
24
  from ..bones.key import KeyBone
17
25
  from ..bones.date import DateBone
18
26
  from ..bones.string import StringBone
19
27
 
28
+ if t.TYPE_CHECKING:
29
+ from .instance import SkeletonInstance
30
+ from .adapter import DatabaseAdapter
31
+ from .meta import KeyType
20
32
 
21
33
  class SeoKeyBone(StringBone):
22
34
  """
@@ -185,7 +197,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
185
197
  if boneInstance.unique:
186
198
  lockValues = boneInstance.getUniquePropertyIndexValues(skel, boneName)
187
199
  for lockValue in lockValues:
188
- dbObj = db.Get(db.Key(f"{skel.kindName}_{boneName}_uniquePropertyIndex", lockValue))
200
+ dbObj = db.get(db.Key(f"{skel.kindName}_{boneName}_uniquePropertyIndex", lockValue))
189
201
  if dbObj and (not skel["key"] or dbObj["references"] != skel["key"].id_or_name):
190
202
  # This value is taken (sadly, not by us)
191
203
  complete = False
@@ -258,7 +270,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
258
270
  except (ValueError, NotImplementedError): # This key did not parse
259
271
  return None
260
272
 
261
- if db_res := db.Get(db_key):
273
+ if db_res := db.get(db_key):
262
274
  skel.setEntity(db_res)
263
275
  return skel
264
276
  elif create in (False, None):
@@ -340,7 +352,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
340
352
  is_add = True
341
353
  else:
342
354
  db_key = db.keyHelper(db_key, skel.kindName)
343
- if db_obj := db.Get(db_key):
355
+ if db_obj := db.get(db_key):
344
356
  skel.dbEntity = db_obj
345
357
  old_copy = {k: v for k, v in skel.dbEntity.items()}
346
358
  is_add = False
@@ -399,7 +411,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
399
411
  new_lock_kind = f"{skel.kindName}_{bone_name}_uniquePropertyIndex"
400
412
  for new_lock_value in new_unique_values:
401
413
  new_lock_key = db.Key(new_lock_kind, new_lock_value)
402
- if lock_db_obj := db.Get(new_lock_key):
414
+ if lock_db_obj := db.get(new_lock_key):
403
415
 
404
416
  # There's already a lock for that value, check if we hold it
405
417
  if lock_db_obj["references"] != skel.dbEntity.key.id_or_name:
@@ -412,7 +424,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
412
424
  # This value is locked for the first time, create a new lock-object
413
425
  lock_obj = db.Entity(new_lock_key)
414
426
  lock_obj["references"] = skel.dbEntity.key.id_or_name
415
- db.Put(lock_obj)
427
+ db.put(lock_obj)
416
428
  if new_lock_value in old_unique_values:
417
429
  old_unique_values.remove(new_lock_value)
418
430
  skel.dbEntity["viur"][f"{bone_name}_uniqueIndexValue"] = new_unique_values
@@ -422,7 +434,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
422
434
  # Try to delete the old lock
423
435
 
424
436
  old_lock_key = db.Key(f"{skel.kindName}_{bone_name}_uniquePropertyIndex", old_unique_value)
425
- if old_lock_obj := db.Get(old_lock_key):
437
+ if old_lock_obj := db.get(old_lock_key):
426
438
  if old_lock_obj["references"] != skel.dbEntity.key.id_or_name:
427
439
 
428
440
  # We've been supposed to have that lock - but we don't.
@@ -430,7 +442,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
430
442
  logging.critical("Detected Database corruption! A Value-Lock had been reassigned!")
431
443
  else:
432
444
  # It's our lock which we don't need anymore
433
- db.Delete(old_lock_key)
445
+ db.delete(old_lock_key)
434
446
  else:
435
447
  logging.critical("Detected Database corruption! Could not delete stale lock-object!")
436
448
 
@@ -526,7 +538,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
526
538
  fixDotNames(skel.dbEntity)
527
539
 
528
540
  # Write the core entry back
529
- db.Put(skel.dbEntity)
541
+ db.put(skel.dbEntity)
530
542
 
531
543
  # Now write the blob-lock object
532
544
  blob_list = skel.preProcessBlobLocks(blob_list)
@@ -537,7 +549,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
537
549
  logging.error(msg)
538
550
  raise ValueError(msg)
539
551
 
540
- if not is_add and (old_blob_lock_obj := db.Get(db.Key("viur-blob-locks", db_key.id_or_name))):
552
+ if not is_add and (old_blob_lock_obj := db.get(db.Key("viur-blob-locks", db_key.id_or_name))):
541
553
  removed_blobs = set(old_blob_lock_obj.get("active_blob_references", [])) - blob_list
542
554
  old_blob_lock_obj["active_blob_references"] = list(blob_list)
543
555
  if old_blob_lock_obj["old_blob_references"] is None:
@@ -550,14 +562,14 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
550
562
 
551
563
  old_blob_lock_obj["has_old_blob_references"] = bool(old_blob_lock_obj["old_blob_references"])
552
564
  old_blob_lock_obj["is_stale"] = False
553
- db.Put(old_blob_lock_obj)
565
+ db.put(old_blob_lock_obj)
554
566
  else: # We need to create a new blob-lock-object
555
567
  blob_lock_obj = db.Entity(db.Key("viur-blob-locks", skel.dbEntity.key.id_or_name))
556
568
  blob_lock_obj["active_blob_references"] = list(blob_list)
557
569
  blob_lock_obj["old_blob_references"] = []
558
570
  blob_lock_obj["has_old_blob_references"] = False
559
571
  blob_lock_obj["is_stale"] = False
560
- db.Put(blob_lock_obj)
572
+ db.put(blob_lock_obj)
561
573
 
562
574
  return skel.dbEntity.key, write_skel, change_list, is_add
563
575
 
@@ -620,7 +632,7 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
620
632
  flushList = []
621
633
  for lockValue in viur_data.get(f"{boneName}_uniqueIndexValue") or []:
622
634
  lockKey = db.Key(f"{skel.kindName}_{boneName}_uniquePropertyIndex", lockValue)
623
- lockObj = db.Get(lockKey)
635
+ lockObj = db.get(lockKey)
624
636
  if not lockObj:
625
637
  logging.error(f"{lockKey=} missing!")
626
638
  elif lockObj["references"] != key.id_or_name:
@@ -629,15 +641,15 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
629
641
  else:
630
642
  flushList.append(lockObj)
631
643
  if flushList:
632
- db.Delete(flushList)
644
+ db.delete(flushList)
633
645
 
634
646
  # Delete the blob-key lock object
635
647
  lockObjectKey = db.Key("viur-blob-locks", key.id_or_name)
636
- lockObj = db.Get(lockObjectKey)
648
+ lockObj = db.get(lockObjectKey)
637
649
 
638
650
  if lockObj is not None:
639
651
  if lockObj["old_blob_references"] is None and lockObj["active_blob_references"] is None:
640
- db.Delete(lockObjectKey) # Nothing to do here
652
+ db.delete(lockObjectKey) # Nothing to do here
641
653
  else:
642
654
  if lockObj["old_blob_references"] is None:
643
655
  # No old stale entries, move active_blob_references -> old_blob_references
@@ -648,9 +660,9 @@ class Skeleton(BaseSkeleton, metaclass=MetaSkel):
648
660
  lockObj["active_blob_references"] = [] # There are no active ones left
649
661
  lockObj["is_stale"] = True
650
662
  lockObj["has_old_blob_references"] = True
651
- db.Put(lockObj)
663
+ db.put(lockObj)
652
664
 
653
- db.Delete(key)
665
+ db.delete(key)
654
666
  processRemovedRelations(key)
655
667
 
656
668
  if key := (key or skel["key"]):
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import typing as t
3
3
 
4
- from viur.core import conf, current, db, email, translate
4
+ from viur.core import conf, current, db, email, translate, utils
5
5
  from .utils import skeletonByKind, listKnownSkeletons
6
6
  from .meta import BaseSkeleton
7
7
 
@@ -58,7 +58,13 @@ def processRemovedRelations(removedKey, cursor=None):
58
58
 
59
59
 
60
60
  @CallDeferred
61
- def updateRelations(destKey: db.Key, minChangeTime: int, changedBone: t.Optional[str], cursor: t.Optional[str] = None):
61
+ def updateRelations(
62
+ dest_key: db.Key,
63
+ min_change_time: int,
64
+ changed_bones: t.Optional[t.Iterable[str] | str] = (),
65
+ cursor: t.Optional[str] = None,
66
+ **kwargs
67
+ ):
62
68
  """
63
69
  This function updates Entities, which may have a copy of values from another entity which has been recently
64
70
  edited (updated). In ViUR, relations are implemented by copying the values from the referenced entity into the
@@ -67,51 +73,60 @@ def updateRelations(destKey: db.Key, minChangeTime: int, changedBone: t.Optional
67
73
  us to track changes made to entities as we might have to update these mirrored values. This is the deferred
68
74
  call from meth:`viur.core.skeleton.Skeleton.write()` after an update (edit) on one Entity to do exactly that.
69
75
 
70
- :param destKey: The database-key of the entity that has been edited
71
- :param minChangeTime: The timestamp on which the edit occurred. As we run deferred, and the entity might have
76
+ :param dest_key: The database-key of the entity that has been edited
77
+ :param min_change_time: The timestamp on which the edit occurred. As we run deferred, and the entity might have
72
78
  been edited multiple times before we get acutally called, we can ignore entities that have been updated
73
- in the meantime as they're already up2date
74
- :param changedBone: If set, we'll update only entites that have a copy of that bone. Relations mirror only
79
+ in the meantime as they're already up-to-date
80
+ :param changed_bones: If set, we'll update only entites that have a copy of that bones. Relations mirror only
75
81
  key and name by default, so we don't have to update these if only another bone has been changed.
76
82
  :param cursor: The database cursor for the current request as we only process five entities at once and then
77
83
  defer again.
78
84
  """
79
- logging.debug(f"Starting updateRelations for {destKey=}; {minChangeTime=}, {changedBone=}, {cursor=}")
85
+ changed_bones = list(utils.ensure_iterable(changed_bones))
86
+ if "changedBone" in kwargs:
87
+ logging.warning("Use of `changedBone` is deprecated; Use `changed_bones` instead!", stacklevel=2)
88
+ changed_bones.extend(utils.ensure_iterable(kwargs["changedBone"]))
89
+ if "minChangeTime" in kwargs:
90
+ logging.warning("Use of `minChangeTime` is deprecated; Use `min_cahnge_time` instead!", stacklevel=2)
91
+ min_change_time = kwargs["minChangeTime"]
92
+ if "destKey" in kwargs:
93
+ logging.warning("Use of `destKey` is deprecated; Use `dest_key` instead!", stacklevel=2)
94
+ dest_key = kwargs["destKey"]
95
+ logging.debug(f"Starting updateRelations for {dest_key=}; {min_change_time=}, {changed_bones=}, {cursor=}")
80
96
  if request_data := current.request_data.get():
81
- request_data["__update_relations_bone"] = changedBone
82
- updateListQuery = (
97
+ request_data["__update_relations_bones"] = changed_bones
98
+ update_list_query = (
83
99
  db.Query("viur-relations")
84
- .filter("dest.__key__ =", destKey)
85
- .filter("viur_delayed_update_tag <", minChangeTime)
100
+ .filter("dest.__key__ =", dest_key)
101
+ .filter("viur_delayed_update_tag <", min_change_time)
86
102
  .filter("viur_relational_updateLevel =", RelationalUpdateLevel.Always.value)
87
103
  )
88
- if changedBone:
89
- updateListQuery.filter("viur_foreign_keys =", changedBone)
104
+ if changed_bones:
105
+ update_list_query.filter("viur_foreign_keys IN", changed_bones)
90
106
  if cursor:
91
- updateListQuery.setCursor(cursor)
92
- updateList = updateListQuery.run(limit=5)
107
+ update_list_query.setCursor(cursor)
108
+ update_list = update_list_query.run(limit=5)
93
109
 
94
- def updateTxn(skel, key, srcRelKey):
95
- if not skel.read(key):
110
+ def __txn_update(_skel, key, srcRelKey):
111
+ if not _skel.read(key):
96
112
  logging.warning(f"Cannot update stale reference to {key=} (referenced from {srcRelKey=})")
97
113
  return
98
114
 
99
- skel.refresh()
100
- skel.write(update_relations=False)
115
+ _skel.refresh()
116
+ _skel.write(update_relations=False)
101
117
 
102
- for srcRel in updateList:
118
+ for src_rel in update_list:
103
119
  try:
104
- skel = skeletonByKind(srcRel["viur_src_kind"])()
120
+ skel = skeletonByKind(src_rel["viur_src_kind"])()
105
121
  except AssertionError:
106
- logging.info(f"""Ignoring {srcRel.key!r} which refers to unknown kind {srcRel["viur_src_kind"]!r}""")
122
+ logging.info(f"""Ignoring {src_rel.key!r} which refers to unknown kind {src_rel["viur_src_kind"]!r}""")
107
123
  continue
108
124
  if db.is_in_transaction():
109
- updateTxn(skel, srcRel["src"].key, srcRel.key)
125
+ __txn_update(skel, src_rel["src"].key, src_rel.key)
110
126
  else:
111
- db.run_in_transaction(updateTxn, skel, srcRel["src"].key, srcRel.key)
112
- nextCursor = updateListQuery.getCursor()
113
- if len(updateList) == 5 and nextCursor:
114
- updateRelations(destKey, minChangeTime, changedBone, nextCursor)
127
+ db.run_in_transaction(__txn_update, skel, src_rel["src"].key, src_rel.key)
128
+ if len(update_list) == 5 and (next_cursor := update_list_query.getCursor()):
129
+ updateRelations(dest_key, min_change_time, changed_bones, next_cursor)
115
130
 
116
131
 
117
132
  @CallableTask
@@ -1,12 +1,14 @@
1
+ import datetime
1
2
  import logging
2
3
  import typing as t
4
+ import urllib.parse
3
5
  import warnings
4
- import datetime
5
6
  from collections.abc import Iterable
6
- from . import string, parse, json # noqa: used by external imports
7
+
7
8
  from viur.core import current, db
8
9
  from viur.core.config import conf
9
10
  from deprecated.sphinx import deprecated
11
+ from . import json, parse, string # noqa: used by external imports
10
12
 
11
13
 
12
14
  def utcNow() -> datetime.datetime:
@@ -114,18 +116,60 @@ def ensure_iterable(
114
116
  if allow_callable and callable(obj):
115
117
  obj = obj()
116
118
 
117
- if isinstance(obj, Iterable): # uses collections.abc.Iterable
119
+ if not isinstance(obj, str) and isinstance(obj, Iterable): # uses collections.abc.Iterable
118
120
  if test is None or test(obj):
119
121
  return obj # return the obj, which is an iterable
120
122
 
121
123
  return () # empty tuple
122
124
 
123
- elif obj is None:
125
+ elif obj is None or (isinstance(obj, str) and not obj):
124
126
  return () # empty tuple
125
127
 
126
128
  return obj, # return a tuple with the obj
127
129
 
128
130
 
131
+ def build_content_disposition_header(
132
+ filename: str,
133
+ *,
134
+ attachment: bool = False,
135
+ inline: bool = False,
136
+ ) -> str:
137
+ """
138
+ Build a Content-Disposition header with UTF-8 support and ASCII fallback.
139
+
140
+ Generates a properly formatted `Content-Disposition` header value, including
141
+ both a fallback ASCII filename and a UTF-8 encoded filename using RFC 5987.
142
+
143
+ Set either `attachment` or `inline` to control content disposition type.
144
+ If both are False, the header will omit disposition type (not recommended).
145
+
146
+ Example:
147
+ filename = "Änderung.pdf" ➜
148
+ 'attachment; filename="Anderung.pdf"; filename*=UTF-8\'\'%C3%84nderung.pdf'
149
+
150
+ :param filename: The desired filename for the content.
151
+ :param attachment: Whether to mark the content as an attachment.
152
+ :param inline: Whether to mark the content as inline.
153
+ :return: A `Content-Disposition` header string.
154
+ """
155
+ if attachment and inline:
156
+ raise ValueError("Only one of 'attachment' or 'inline' may be True.")
157
+
158
+ fallback = string.normalize_ascii(filename)
159
+ quoted_utf8 = urllib.parse.quote_from_bytes(filename.encode("utf-8"))
160
+
161
+ content_disposition = "; ".join(
162
+ item for item in (
163
+ "attachment" if attachment else None,
164
+ "inline" if inline else None,
165
+ f'filename="{fallback}"' if filename else None,
166
+ f'filename*=UTF-8\'\'{quoted_utf8}' if filename else None,
167
+ ) if item
168
+ )
169
+
170
+ return content_disposition
171
+
172
+
129
173
  # DEPRECATED ATTRIBUTES HANDLING
130
174
  __UTILS_CONF_REPLACEMENT = {
131
175
  "projectID": "viur.instance.project_id",
@@ -4,6 +4,7 @@ ViUR utility functions regarding string processing.
4
4
  import re
5
5
  import secrets
6
6
  import string
7
+ import unicodedata
7
8
  import warnings
8
9
 
9
10
 
@@ -85,6 +86,28 @@ def unescape(val: str) -> str:
85
86
  return re.sub(r"&(\w{2,4}|#0*(\d{2}));", __escape_replace, str(val).strip())
86
87
 
87
88
 
89
+ def normalize_ascii(text: str) -> str:
90
+ """
91
+ Normalize a Unicode string to an ASCII-only version.
92
+
93
+ This function uses NFKD normalization to decompose accented and special characters,
94
+ then encodes the result to ASCII, ignoring any characters that can't be represented.
95
+
96
+ For example:
97
+ "Änderung" -> "Anderung"
98
+ "Café" -> "Cafe"
99
+
100
+ :param text: The input Unicode string to normalize.
101
+ :return: A normalized ASCII-only version of the input string.
102
+ """
103
+ return (
104
+ unicodedata
105
+ .normalize("NFKD", text)
106
+ .encode("ascii", "ignore")
107
+ .decode("ascii")
108
+ )
109
+
110
+
88
111
  def is_prefix(name: str, prefix: str, delimiter: str = ".") -> bool:
89
112
  """
90
113
  Utility function to check if a given name matches a prefix,
@@ -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.8.0.dev6"
6
+ __version__ = "3.8.0.dev7"
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.8.0.dev6
3
+ Version: 3.8.0.dev7
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>
File without changes
File without changes
File without changes