viur-core 3.8.0.dev7__tar.gz → 3.8.0.dev9__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.dev7 → viur_core-3.8.0.dev9}/PKG-INFO +5 -5
  2. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/pyproject.toml +4 -4
  3. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/file.py +2 -1
  4. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/record.py +1 -1
  5. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/config.py +5 -3
  6. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/cache.py +67 -61
  7. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/query.py +48 -21
  8. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/types.py +6 -2
  9. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/file.py +110 -49
  10. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/scripts/viur_migrate.py +0 -1
  11. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/relskel.py +18 -3
  12. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/skeleton.py +14 -14
  13. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/tasks.py +136 -125
  14. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/version.py +1 -1
  15. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/PKG-INFO +5 -5
  16. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/requires.txt +2 -1
  17. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/tests/test_config.py +0 -1
  18. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/LICENSE +0 -0
  19. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/README.md +0 -0
  20. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/setup.cfg +0 -0
  21. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/__init__.py +0 -0
  22. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/__init__.py +0 -0
  23. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/base.py +0 -0
  24. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/boolean.py +0 -0
  25. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/captcha.py +0 -0
  26. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/color.py +0 -0
  27. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/credential.py +0 -0
  28. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/date.py +0 -0
  29. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/email.py +0 -0
  30. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/image.py +0 -0
  31. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/json.py +0 -0
  32. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/key.py +0 -0
  33. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/numeric.py +0 -0
  34. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/password.py +0 -0
  35. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/phone.py +0 -0
  36. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/randomslice.py +0 -0
  37. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/raw.py +0 -0
  38. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/relational.py +0 -0
  39. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/select.py +0 -0
  40. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/selectcountry.py +0 -0
  41. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/sortindex.py +0 -0
  42. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/spam.py +0 -0
  43. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/spatial.py +0 -0
  44. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/string.py +0 -0
  45. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/text.py +0 -0
  46. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/treeleaf.py +0 -0
  47. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/treenode.py +0 -0
  48. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/uid.py +0 -0
  49. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/uri.py +0 -0
  50. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/bones/user.py +0 -0
  51. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/cache.py +0 -0
  52. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/current.py +0 -0
  53. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/__init__.py +0 -0
  54. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/config.py +0 -0
  55. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/overrides.py +0 -0
  56. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/transport.py +0 -0
  57. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/db/utils.py +0 -0
  58. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/decorators.py +0 -0
  59. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/email.py +0 -0
  60. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/errors.py +0 -0
  61. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/i18n.py +0 -0
  62. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/languages/__init__.py +0 -0
  63. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/languages/de.py +0 -0
  64. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/languages/en.py +0 -0
  65. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/logging.py +0 -0
  66. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/module.py +0 -0
  67. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/__init__.py +0 -0
  68. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/formmailer.py +0 -0
  69. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/history.py +0 -0
  70. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/moduleconf.py +0 -0
  71. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/page.py +0 -0
  72. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/script.py +0 -0
  73. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/site.py +0 -0
  74. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/translation.py +0 -0
  75. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/modules/user.py +0 -0
  76. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/pagination.py +0 -0
  77. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/__init__.py +0 -0
  78. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/instanced_module.py +0 -0
  79. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/list.py +0 -0
  80. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/singleton.py +0 -0
  81. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/skelmodule.py +0 -0
  82. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/prototypes/tree.py +0 -0
  83. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/ratelimit.py +0 -0
  84. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/__init__.py +0 -0
  85. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/abstract.py +0 -0
  86. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/__init__.py +0 -0
  87. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/default.py +0 -0
  88. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/__init__.py +0 -0
  89. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/date.py +0 -0
  90. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/debug.py +0 -0
  91. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/regex.py +0 -0
  92. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/session.py +0 -0
  93. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/strings.py +0 -0
  94. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/tests.py +0 -0
  95. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/env/viur.py +0 -0
  96. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/user.py +0 -0
  97. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/html/utils.py +0 -0
  98. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/json/__init__.py +0 -0
  99. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/json/default.py +0 -0
  100. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/json/user.py +0 -0
  101. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/vi/__init__.py +0 -0
  102. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/render/vi/user.py +0 -0
  103. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/request.py +0 -0
  104. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/secret.py +0 -0
  105. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/securityheaders.py +0 -0
  106. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/securitykey.py +0 -0
  107. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/session.py +0 -0
  108. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/__init__.py +0 -0
  109. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/adapter.py +0 -0
  110. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/instance.py +0 -0
  111. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/meta.py +0 -0
  112. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/skeleton/utils.py +0 -0
  113. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/tasks.py +0 -0
  114. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/template/error.html +0 -0
  115. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/template/vi_user_google_login.html +0 -0
  116. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/utils/__init__.py +0 -0
  117. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/utils/json.py +0 -0
  118. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/utils/parse.py +0 -0
  119. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur/core/utils/string.py +0 -0
  120. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/SOURCES.txt +0 -0
  121. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/dependency_links.txt +0 -0
  122. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/entry_points.txt +0 -0
  123. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/src/viur_core.egg-info/top_level.txt +0 -0
  124. {viur_core-3.8.0.dev7 → viur_core-3.8.0.dev9}/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.dev7
3
+ Version: 3.8.0.dev9
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>
@@ -34,11 +34,10 @@ Classifier: Development Status :: 5 - Production/Stable
34
34
  Classifier: Intended Audience :: Developers
35
35
  Classifier: License :: OSI Approved :: MIT License
36
36
  Classifier: Operating System :: OS Independent
37
- Classifier: Programming Language :: Python :: 3.10
38
- Classifier: Programming Language :: Python :: 3.11
39
37
  Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
40
39
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
41
- Requires-Python: >=3.10
40
+ Requires-Python: >=3.12
42
41
  Description-Content-Type: text/markdown
43
42
  License-File: LICENSE
44
43
  Requires-Dist: appengine-python-standard~=1.0
@@ -57,6 +56,7 @@ Requires-Dist: googleapis-common-protos[grpc]~=1.0
57
56
  Requires-Dist: gunicorn~=23.0
58
57
  Requires-Dist: jinja2~=3.0
59
58
  Requires-Dist: jsonschema~=4.0
59
+ Requires-Dist: logics-py==0.0.12
60
60
  Requires-Dist: pillow~=10.0
61
61
  Requires-Dist: pyotp~=2.0
62
62
  Requires-Dist: pytz~=2023.0
@@ -64,7 +64,7 @@ Requires-Dist: pyyaml~=6.0
64
64
  Requires-Dist: qrcode~=7.0
65
65
  Requires-Dist: requests~=2.0
66
66
  Requires-Dist: tzlocal~=5.0
67
- Requires-Dist: urllib3==1.26.18
67
+ Requires-Dist: urllib3==1.26.20
68
68
  Requires-Dist: user-agents~=2.0
69
69
  Requires-Dist: webob~=1.0
70
70
  Provides-Extra: mailjet
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "gunicorn~=23.0",
23
23
  "jinja2~=3.0",
24
24
  "jsonschema~=4.0",
25
+ "logics-py==0.0.12",
25
26
  "pillow~=10.0",
26
27
  "pyotp~=2.0",
27
28
  "pytz~=2023.0",
@@ -29,11 +30,11 @@ dependencies = [
29
30
  "qrcode~=7.0",
30
31
  "requests~=2.0",
31
32
  "tzlocal~=5.0",
32
- "urllib3==1.26.18", # for appengine-python-standard used by some projects (https://github.com/GoogleCloudPlatform/appengine-python-standard/blob/main/setup.py#L28)
33
+ "urllib3==1.26.20", # for appengine-python-standard (https://github.com/GoogleCloudPlatform/appengine-python-standard/blob/main/setup.py#L28)
33
34
  "user-agents~=2.0",
34
35
  "webob~=1.0",
35
36
  ]
36
- requires-python = ">=3.10"
37
+ requires-python = ">=3.12"
37
38
  authors = [
38
39
  { name = "Mausbrand Informationssysteme GmbH", email = "devs@viur.dev" },
39
40
  ]
@@ -49,9 +50,8 @@ classifiers = [
49
50
  "Intended Audience :: Developers",
50
51
  "License :: OSI Approved :: MIT License",
51
52
  "Operating System :: OS Independent",
52
- "Programming Language :: Python :: 3.10",
53
- "Programming Language :: Python :: 3.11",
54
53
  "Programming Language :: Python :: 3.12",
54
+ "Programming Language :: Python :: 3.13",
55
55
  "Topic :: Software Development :: Libraries :: Application Frameworks",
56
56
  ]
57
57
 
@@ -229,9 +229,10 @@ class FileBone(TreeLeafBone):
229
229
  super().postSavedHandler(skel, boneName, key)
230
230
  if (
231
231
  current.request.get().is_deferred
232
- and "derived" in current.request_data.get().get("__update_relations_bones")
232
+ and "derived" in (current.request_data.get().get("__update_relations_bones") or ())
233
233
  ):
234
234
  return
235
+
235
236
  from viur.core.skeleton import RelSkel, Skeleton
236
237
 
237
238
  if issubclass(skel.skeletonCls, Skeleton):
@@ -31,7 +31,7 @@ class RecordBone(BaseBone):
31
31
  using: 'viur.core.skeleton.RelSkel' = None,
32
32
  **kwargs
33
33
  ):
34
- from viur.core.skeleton import RelSkel
34
+ from viur.core.skeleton.relskel import RelSkel
35
35
  if not issubclass(using, RelSkel):
36
36
  raise ValueError("RecordBone requires for valid using-parameter (subclass of viur.core.skeleton.RelSkel)")
37
37
 
@@ -849,8 +849,11 @@ class Conf(ConfigType):
849
849
  ]
850
850
  """Backward compatibility flags; Remove to enforce new style."""
851
851
 
852
- db_engine: str = "viur.datastore"
853
- """Database engine module"""
852
+ db_query_external_limit: int = 100
853
+ """Sets the maximum query limit allowed by external filters."""
854
+
855
+ db_query_default_limit: int = 30
856
+ """Sets the default query limit for all queries."""
854
857
 
855
858
  db_memcache_client: Client | None = None
856
859
  """If set, ViUR cache data for the db.get in the Memcache for faster access."""
@@ -1031,7 +1034,6 @@ class Conf(ConfigType):
1031
1034
  "viur.cacheEnvironmentKey": "cache_environment_key",
1032
1035
  "viur.contentSecurityPolicy": "content_security_policy",
1033
1036
  "viur.bone.boolean.str2true": "bone_boolean_str2true",
1034
- "viur.db.engine": "db_engine",
1035
1037
  "viur.errorHandler": "error_handler",
1036
1038
  "viur.static.embedSvg.path": "static_embed_svg_path",
1037
1039
  "viur.file.hmacKey": "file_hmac_key",
@@ -1,6 +1,9 @@
1
+ import datetime
2
+
3
+ from google.appengine.ext.testbed import Testbed
4
+
1
5
  import logging
2
6
  import sys
3
- import time
4
7
  import typing as t
5
8
 
6
9
  from viur.core.config import conf
@@ -8,9 +11,9 @@ from .types import Entity, Key
8
11
 
9
12
  MEMCACHE_MAX_BATCH_SIZE = 30
10
13
  MEMCACHE_NAMESPACE = "viur-datastore"
11
- MEMCACHE_TIMEOUT = 60 * 60
12
- MEMCACHE_MAX_SIZE = 1_000_000
13
-
14
+ MEMCACHE_TIMEOUT: int | datetime.timedelta = datetime.timedelta(days=1)
15
+ MEMCACHE_MAX_SIZE: t.Final[int] = 1_000_000
16
+ TESTBED = None
14
17
  """
15
18
 
16
19
  This Module controls the Interaction with the Memcache from Google
@@ -18,12 +21,8 @@ MEMCACHE_MAX_SIZE = 1_000_000
18
21
  .. code-block:: python
19
22
  # Example
20
23
  from viur.core import conf
21
- if not conf.instance.is_dev_server:
22
- from google.appengine.api.memcache import Client
23
- conf.db_memcache_client = Client()
24
- else:
25
- conf.db_memcache_client = db.cache.LocalMemcache()
26
-
24
+ from google.appengine.api.memcache import Client
25
+ conf.db_memcache_client = Client()
27
26
  """
28
27
 
29
28
  __all__ = [
@@ -34,40 +33,63 @@ __all__ = [
34
33
  "get",
35
34
  "put",
36
35
  "delete",
37
- "LocalMemcache",
36
+ "flush",
38
37
  ]
39
38
 
40
39
 
41
- def get(keys: t.Union[str, Key, t.List[str], t.List[Key]]) -> t.Dict[str, dict]:
40
+ def get(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> t.Union[Entity, list[Entity], None]:
42
41
  """
43
42
  Reads data form the memcache.
44
43
  :param keys: Unique identifier(s) for one or more entry(s).
45
- :return: A dict with the entry(s) that found in the memcache.
44
+ :param namespace: Optional namespace to use.
45
+ :return: The entity (or None if it has not been found), or a list of entities.
46
46
  """
47
47
  if not check_for_memcache():
48
- return {}
49
- if not isinstance(keys, list):
48
+ return None
49
+
50
+ namespace = namespace or MEMCACHE_NAMESPACE
51
+ single_request = not isinstance(keys, (list, tuple, set))
52
+ if single_request:
50
53
  keys = [keys]
51
54
  keys = [str(key) for key in keys] # Enforce that all keys are strings
52
- res = {}
55
+ cached_data = {}
56
+ result = []
53
57
  try:
54
58
  while keys:
55
- res |= conf.db_memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=MEMCACHE_NAMESPACE)
59
+ cached_data |= conf.db_memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace)
56
60
  keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
57
61
  except Exception as e:
58
62
  logging.error(f"""Failed to get keys form the memcache with {e=}""")
59
- return res
60
-
61
-
62
- def put(data: t.Union[Entity, t.Dict[Key, Entity], t.List[Entity]]):
63
+ for key, value in cached_data.items():
64
+ entity = Entity(Key(key))
65
+ entity |= value
66
+ result.append(entity)
67
+ if single_request:
68
+ return result[0] if result else None
69
+ return result if result else None
70
+
71
+
72
+ def put(
73
+ data: t.Union[Entity, t.Dict[Key, Entity], t.Iterable[Entity]],
74
+ namespace: t.Optional[str] = None,
75
+ timeout: t.Optional[t.Union[int, datetime.timedelta]] = None
76
+ ) -> bool:
63
77
  """
64
78
  Writes Data to the memcache.
65
-
66
79
  :param data: Data to write
80
+ :param namespace: Optional namespace to use.
81
+ :param timeout: Optional timeout in seconds or a timedelta object.
82
+ :return: A boolean indicating success.
67
83
  """
68
84
  if not check_for_memcache():
69
- return
70
- if isinstance(data, list):
85
+ return False
86
+
87
+ namespace = namespace or MEMCACHE_NAMESPACE
88
+ timeout = timeout or MEMCACHE_TIMEOUT
89
+ if isinstance(timeout, datetime.timedelta):
90
+ timeout = timeout.total_seconds()
91
+
92
+ if isinstance(data, (list, tuple, set)):
71
93
  data = {item.key: item for item in data}
72
94
  elif isinstance(data, Entity):
73
95
  data = {data.key: data}
@@ -80,41 +102,48 @@ def put(data: t.Union[Entity, t.Dict[Key, Entity], t.List[Entity]]):
80
102
  try:
81
103
  while keys:
82
104
  data_batch = {key: data[key] for key in keys[:MEMCACHE_MAX_BATCH_SIZE]}
83
- conf.db_memcache_client.set_multi(data_batch, namespace=MEMCACHE_NAMESPACE, time=MEMCACHE_TIMEOUT)
105
+ conf.db_memcache_client.set_multi(data_batch, namespace=namespace, time=timeout)
84
106
  keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
107
+ return True
85
108
  except Exception as e:
86
109
  logging.error(f"""Failed to put data to the memcache with {e=}""")
110
+ return False
87
111
 
88
112
 
89
- def delete(keys: t.Union[str, Key, t.List[str], t.List[Key]]) -> None:
113
+ def delete(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> None:
90
114
  """
91
115
  Deletes an Entry form memcache.
92
-
93
116
  :param keys: Unique identifier(s) for one or more entry(s).
117
+ :param namespace: Optional namespace to use.
94
118
  """
95
119
  if not check_for_memcache():
96
- return
120
+ return None
121
+
122
+ namespace = namespace or MEMCACHE_NAMESPACE
97
123
  if not isinstance(keys, list):
98
124
  keys = [keys]
99
125
  keys = [str(key) for key in keys] # Enforce that all keys are strings
100
126
  try:
101
127
  while keys:
102
- conf.db_memcache_client.delete_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=MEMCACHE_NAMESPACE)
128
+ conf.db_memcache_client.delete_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace)
103
129
  keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
104
130
  except Exception as e:
105
131
  logging.error(f"""Failed to delete keys form the memcache with {e=}""")
106
132
 
107
133
 
108
- def flush():
134
+ def flush() -> bool:
109
135
  """
110
136
  Deletes everything in memcache.
137
+ :return: A boolean indicating success.
111
138
  """
112
139
  if not check_for_memcache():
113
- return
140
+ return False
114
141
  try:
115
142
  conf.db_memcache_client.flush_all()
116
143
  except Exception as e:
117
144
  logging.error(f"""Failed to flush the memcache with {e=}""")
145
+ return False
146
+ return True
118
147
 
119
148
 
120
149
  def get_size(obj: t.Any) -> int:
@@ -133,36 +162,13 @@ def check_for_memcache() -> bool:
133
162
  if conf.db_memcache_client is None:
134
163
  logging.warning(f"""conf.db_memcache_client is 'None'. It can not be used.""")
135
164
  return False
165
+ init_testbed()
136
166
  return True
137
167
 
138
168
 
139
- class LocalMemcache:
140
- def __init__(self):
141
- self._data = {}
142
-
143
- def get_multi(self, keys: t.List[str], namespace: str = MEMCACHE_NAMESPACE):
144
- self._data.setdefault(namespace, {})
145
- res = {}
146
- for key in keys:
147
- if (data := self._data[namespace].get(key)) is not None:
148
- if data["__lifetime__"]["last_seen"] + data["__lifetime__"]["timeout"] > time.time():
149
- res[key] = data["__data__"]
150
- else:
151
- self._data[namespace].pop(key)
152
- return res
153
-
154
- def set_multi(self, data: t.Dict[str, t.Any], namespace: str = MEMCACHE_NAMESPACE, time: int = MEMCACHE_TIMEOUT):
155
- self._data.setdefault(namespace, {})
156
- for key, value in data.items():
157
- self._data[namespace][key] = {}
158
- self._data[namespace][key]["__data__"] = value
159
- self._data[namespace][key]["__lifetime__"] = {"timeout": time, "last_seen": time.time()}
160
-
161
- def delete_multi(self, keys: t.List[str] = [], namespace: str = MEMCACHE_NAMESPACE):
162
- self._data.setdefault(namespace, {})
163
- for key in keys:
164
- if (data := self._data[namespace].get(key)) is not None:
165
- self._data[namespace].pop(key)
166
-
167
- def flush_all(self):
168
- self._data.clear()
169
+ def init_testbed() -> None:
170
+ global TESTBED
171
+ if TESTBED is None and conf.instance.is_dev_server and conf.db_memcache_client:
172
+ TESTBED = Testbed()
173
+ TESTBED.activate()
174
+ TESTBED.init_memcache_stub()
@@ -151,9 +151,12 @@ class Query(object):
151
151
  """
152
152
  if self.srcSkel is None:
153
153
  raise NotImplementedError("This query has not been created using skel.all()")
154
+
154
155
  if self.queries is None: # This query is already unsatisfiable and adding more constraints won't change this
155
156
  return self
157
+
156
158
  skel = self.srcSkel
159
+
157
160
  if "search" in filters:
158
161
  if self.srcSkel.customDatabaseAdapter and self.srcSkel.customDatabaseAdapter.providesFulltextSearch:
159
162
  self._fulltextQueryString = str(filters["search"])
@@ -163,31 +166,50 @@ class Query(object):
163
166
  % self.srcSkel.kindName
164
167
  )
165
168
  self.queries = None
169
+
166
170
  bones = [(y, x) for x, y in skel.items()]
171
+
167
172
  try:
168
173
  # Process filters first
169
174
  for bone, key in bones:
170
175
  bone.buildDBFilter(key, skel, self, filters)
176
+
171
177
  # Parse orders
172
178
  for bone, key in bones:
173
179
  bone.buildDBSort(key, skel, self, filters)
180
+
174
181
  except RuntimeError as e:
175
182
  logging.exception(e)
176
183
  self.queries = None
177
184
  return self
185
+
178
186
  startCursor = endCursor = None
187
+
179
188
  if "cursor" in filters and filters["cursor"] and filters["cursor"].lower() != "none":
180
189
  startCursor = filters["cursor"]
190
+
181
191
  if "endcursor" in filters and filters["endcursor"] and filters["endcursor"].lower() != "none":
182
192
  endCursor = filters["endcursor"]
193
+
183
194
  if startCursor or endCursor:
184
195
  self.setCursor(startCursor, endCursor)
185
- if (
186
- "limit" in filters
187
- and str(filters["limit"]).isdigit()
188
- and 0 < int(filters["limit"]) <= 100
189
- ):
190
- self.limit(int(filters["limit"]))
196
+
197
+ if limit := filters.get("limit"):
198
+ try:
199
+ limit = int(limit)
200
+
201
+ # disallow limit beyond conf.db_query_external_limit
202
+ if limit > conf.db_query_external_limit:
203
+ limit = conf.db_query_external_limit
204
+
205
+ # forbid any limit < 0, which might bypass defaults
206
+ if limit < 0:
207
+ limit = 0
208
+
209
+ self.limit(limit)
210
+ except ValueError:
211
+ pass # ignore this
212
+
191
213
  return self
192
214
 
193
215
  def filter(self, prop: str, value: DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]) -> t.Self:
@@ -373,19 +395,17 @@ class Query(object):
373
395
 
374
396
  def limit(self, limit: int) -> t.Self:
375
397
  """
376
- Sets the query limit to *amount* entities in the result.
398
+ Sets the query limit to *limit* entities in the result.
377
399
 
378
- Specifying a limit of 0 disables the limit (use with care!).
379
-
380
- :param limit: The maximum number of entities.
400
+ :param limit: The maximum number of entities per batch.
381
401
  :returns: Returns the query itself for chaining.
382
402
  """
383
- # TODO Add a check for the limit (<=100) ?
384
403
  if isinstance(self.queries, QueryDefinition):
385
404
  self.queries.limit = limit
386
405
  elif isinstance(self.queries, list):
387
406
  for query in self.queries:
388
407
  query.limit = limit
408
+
389
409
  return self
390
410
 
391
411
  def distinctOn(self, keyList: t.List[str]) -> t.Self:
@@ -586,36 +606,46 @@ class Query(object):
586
606
  if self._fulltextQueryString:
587
607
  if utils.is_in_transaction():
588
608
  raise ValueError("Can't run fulltextSearch inside transactions!") # InvalidStateError FIXME!
609
+
589
610
  qryStr = self._fulltextQueryString
590
611
  self._fulltextQueryString = None # Reset, so the adapter can still work with this query
591
612
  res = self.srcSkel.customDatabaseAdapter.fulltextSearch(qryStr, self)
613
+
592
614
  if not self.srcSkel.customDatabaseAdapter.fulltextSearchGuaranteesQueryConstrains:
593
615
  # Search might yield results that are not included in the listfilter
594
616
  if isinstance(self.queries, QueryDefinition): # Just one
595
617
  res = [x for x in res if _entryMatchesQuery(x, self.queries.filters)]
596
618
  else: # Multi-Query, must match at least one
597
619
  res = [x for x in res if any([_entryMatchesQuery(x, y.filters) for y in self.queries])]
620
+
598
621
  elif isinstance(self.queries, list):
622
+ limit = limit if limit >= 0 else self.queries[0].limit
623
+
599
624
  # We have more than one query to run
600
625
  if self._calculateInternalMultiQueryLimit:
601
- limit = self._calculateInternalMultiQueryLimit(self, limit if limit != -1 else self.queries[0].limit)
626
+ limit = self._calculateInternalMultiQueryLimit(self, limit)
627
+
602
628
  res = []
603
629
  # We run all queries first (preventing multiple round-trips to the server)
604
630
  for singleQuery in self.queries:
605
- res.append(self._run_single_filter_query(singleQuery, limit if limit != -1 else singleQuery.limit))
631
+ res.append(self._run_single_filter_query(singleQuery, limit))
632
+
606
633
  # Wait for the actual results to arrive and convert the protobuffs to Entries
607
634
  res = [self._fixKind(x) for x in res]
608
635
  if self._customMultiQueryMerge:
609
636
  # We have a custom merge function, use that
610
- res = self._customMultiQueryMerge(self, res, limit if limit != -1 else self.queries[0].limit)
637
+ res = self._customMultiQueryMerge(self, res, limit)
611
638
  else:
612
639
  # We must merge (and sort) the results ourself
613
640
  res = self._merge_multi_query_results(res)
641
+
614
642
  else: # We have just one single query
615
643
  res = self._fixKind(self._run_single_filter_query(
616
- self.queries, limit if limit != -1 else self.queries.limit))
644
+ self.queries, limit if limit >= 0 else self.queries.limit))
645
+
617
646
  if res:
618
647
  self._lastEntry = res[-1]
648
+
619
649
  return res
620
650
 
621
651
  def count(self, up_to: int = 2 ** 63 - 1) -> int:
@@ -648,7 +678,6 @@ class Query(object):
648
678
  should be used.
649
679
 
650
680
  :param limit: Limits the query to the defined maximum entities.
651
- A maximum value of 100 entries can be fetched at once.
652
681
 
653
682
  :raises: :exc:`BadFilterError` if a filter string is invalid
654
683
  :raises: :exc:`BadValueError` if a filter value is invalid.
@@ -659,19 +688,17 @@ class Query(object):
659
688
 
660
689
  if self.srcSkel is None:
661
690
  raise NotImplementedError("This query has not been created using skel.all()")
662
- # limit = limit if limit != -1 else self._limit
663
- if limit != -1 and not (0 < limit <= 100):
664
- logging.error(("Limit", limit))
665
- raise NotImplementedError(
666
- "This query is not limited! You must specify an upper bound using limit() between 1 and 100")
667
691
 
668
692
  res = SkelList(self.srcSkel)
693
+
669
694
  for entity in self.run(limit):
670
695
  skel_instance = SkeletonInstance(self.srcSkel.skeletonCls, bone_map=self.srcSkel.boneMap)
671
696
  skel_instance.dbEntity = entity
672
697
  res.append(skel_instance)
698
+
673
699
  res.getCursor = lambda: self.getCursor()
674
700
  res.get_orders = lambda: self.get_orders()
701
+
675
702
  return res
676
703
 
677
704
  def iter(self) -> t.Iterator[Entity]:
@@ -7,7 +7,8 @@ import datetime
7
7
  import enum
8
8
  import typing as t
9
9
  from contextvars import ContextVar
10
- from dataclasses import dataclass
10
+ from dataclasses import dataclass, field
11
+ from ..config import conf
11
12
 
12
13
  from google.cloud.datastore import Entity as Datastore_entity, Key as Datastore_key
13
14
 
@@ -167,7 +168,7 @@ class QueryDefinition:
167
168
  distinct: t.Optional[list[str]] = None
168
169
  """If set, a list of fields that we should return distinct values of"""
169
170
 
170
- limit: int = 30
171
+ limit: int = field(init=False)
171
172
  """The maximum amount of entities that should be returned"""
172
173
 
173
174
  startCursor: t.Optional[str] = None
@@ -178,3 +179,6 @@ class QueryDefinition:
178
179
 
179
180
  currentCursor: t.Optional[str] = None
180
181
  """Will be set after this query has been run, pointing after the last entity returned"""
182
+
183
+ def __post_init__(self):
184
+ self.limit = conf.db_query_default_limit