viur-core 3.9.0.dev4__tar.gz → 3.9.0.dev6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {viur_core-3.9.0.dev4/src/viur_core.egg-info → viur_core-3.9.0.dev6}/PKG-INFO +9 -8
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/pyproject.toml +8 -7
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/__init__.py +3 -0
- viur_core-3.9.0.dev6/src/viur/core/bones/address.py +105 -0
- viur_core-3.9.0.dev6/src/viur/core/bones/captcha.py +177 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/numeric.py +102 -22
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/randomslice.py +2 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/relational.py +16 -9
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/spatial.py +12 -6
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/text.py +1 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/config.py +1 -7
- viur_core-3.9.0.dev6/src/viur/core/data/zip_patterns.py +257 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/__init__.py +2 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/query.py +131 -73
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/transport.py +40 -14
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/types.py +12 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/utils.py +14 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/module.py +3 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/file.py +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/history.py +42 -13
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/script.py +7 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/translation.py +2 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/user.py +3 -3
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/skelmodule.py +12 -11
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/json/default.py +5 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/securityheaders.py +14 -3
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/__init__.py +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/instance.py +70 -9
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/meta.py +36 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/skeleton.py +14 -5
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/utils.py +20 -10
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/tasks.py +4 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6/src/viur_core.egg-info}/PKG-INFO +9 -8
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/SOURCES.txt +3 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/requires.txt +7 -7
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/tests/test_config.py +0 -1
- viur_core-3.9.0.dev6/tests/test_db.py +433 -0
- viur_core-3.9.0.dev6/tests/test_json_encoder.py +146 -0
- viur_core-3.9.0.dev4/src/viur/core/bones/captcha.py +0 -130
- viur_core-3.9.0.dev4/tests/test_db.py +0 -29
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/LICENSE +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/README.md +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/setup.cfg +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/base.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/boolean.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/code.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/record.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/string.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/cache.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/contrib/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/contrib/loginkey.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/contrib/ratelimit.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/cache.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/render/vi/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/request.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/session.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/tasks.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/utils/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/top_level.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/tests/test_decorators.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/tests/test_errors.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev6}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viur-core
|
|
3
|
-
Version: 3.9.0.
|
|
3
|
+
Version: 3.9.0.dev6
|
|
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>
|
|
@@ -36,11 +36,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
36
36
|
Classifier: Operating System :: OS Independent
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.12
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
39
40
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
40
41
|
Requires-Python: >=3.12
|
|
41
42
|
Description-Content-Type: text/markdown
|
|
42
43
|
License-File: LICENSE
|
|
43
|
-
Requires-Dist: appengine-python-standard~=
|
|
44
|
+
Requires-Dist: appengine-python-standard~=2.0
|
|
44
45
|
Requires-Dist: Deprecated~=1.2
|
|
45
46
|
Requires-Dist: google-api-core[grpc]~=2.0
|
|
46
47
|
Requires-Dist: google-auth~=2.0
|
|
@@ -48,23 +49,23 @@ Requires-Dist: google-cloud-bigquery~=3.0
|
|
|
48
49
|
Requires-Dist: google-cloud-datastore~=2.0
|
|
49
50
|
Requires-Dist: google-cloud-iam~=2.0
|
|
50
51
|
Requires-Dist: google-cloud-logging~=3.0
|
|
52
|
+
Requires-Dist: google-cloud-recaptcha-enterprise~=1.0
|
|
51
53
|
Requires-Dist: google-cloud-secret-manager~=2.0
|
|
52
|
-
Requires-Dist: google-cloud-storage
|
|
54
|
+
Requires-Dist: google-cloud-storage<4,>=2
|
|
53
55
|
Requires-Dist: google-cloud-tasks~=2.0
|
|
54
56
|
Requires-Dist: google-resumable-media~=2.0
|
|
55
57
|
Requires-Dist: googleapis-common-protos[grpc]~=1.0
|
|
56
|
-
Requires-Dist: gunicorn
|
|
58
|
+
Requires-Dist: gunicorn<27,>=23
|
|
57
59
|
Requires-Dist: jinja2~=3.0
|
|
58
60
|
Requires-Dist: jsonschema~=4.0
|
|
59
61
|
Requires-Dist: logics-py==0.0.12
|
|
60
|
-
Requires-Dist: pillow
|
|
62
|
+
Requires-Dist: pillow<13,>=11
|
|
61
63
|
Requires-Dist: pyotp~=2.0
|
|
62
|
-
Requires-Dist: pytz
|
|
64
|
+
Requires-Dist: pytz>=2023
|
|
63
65
|
Requires-Dist: pyyaml~=6.0
|
|
64
|
-
Requires-Dist: qrcode
|
|
66
|
+
Requires-Dist: qrcode<9,>=7
|
|
65
67
|
Requires-Dist: requests~=2.0
|
|
66
68
|
Requires-Dist: tzlocal~=5.0
|
|
67
|
-
Requires-Dist: urllib3==1.26.20
|
|
68
69
|
Requires-Dist: user-agents~=2.0
|
|
69
70
|
Requires-Dist: webob~=1.0
|
|
70
71
|
Provides-Extra: mailjet
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "viur-core"
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
dependencies = [
|
|
9
|
-
"appengine-python-standard~=
|
|
9
|
+
"appengine-python-standard~=2.0",
|
|
10
10
|
"Deprecated~=1.2",
|
|
11
11
|
"google-api-core[grpc]~=2.0",
|
|
12
12
|
"google-auth~=2.0",
|
|
@@ -14,23 +14,23 @@ dependencies = [
|
|
|
14
14
|
"google-cloud-datastore~=2.0",
|
|
15
15
|
"google-cloud-iam~=2.0",
|
|
16
16
|
"google-cloud-logging~=3.0",
|
|
17
|
+
"google-cloud-recaptcha-enterprise~=1.0",
|
|
17
18
|
"google-cloud-secret-manager~=2.0",
|
|
18
|
-
"google-cloud-storage
|
|
19
|
+
"google-cloud-storage>=2,<4",
|
|
19
20
|
"google-cloud-tasks~=2.0",
|
|
20
21
|
"google-resumable-media~=2.0",
|
|
21
22
|
"googleapis-common-protos[grpc]~=1.0",
|
|
22
|
-
"gunicorn
|
|
23
|
+
"gunicorn>=23,<27",
|
|
23
24
|
"jinja2~=3.0",
|
|
24
25
|
"jsonschema~=4.0",
|
|
25
26
|
"logics-py==0.0.12",
|
|
26
|
-
"pillow
|
|
27
|
+
"pillow>=11,<13",
|
|
27
28
|
"pyotp~=2.0",
|
|
28
|
-
"pytz
|
|
29
|
+
"pytz>=2023",
|
|
29
30
|
"pyyaml~=6.0",
|
|
30
|
-
"qrcode
|
|
31
|
+
"qrcode>=7,<9",
|
|
31
32
|
"requests~=2.0",
|
|
32
33
|
"tzlocal~=5.0",
|
|
33
|
-
"urllib3==1.26.20", # for appengine-python-standard (https://github.com/GoogleCloudPlatform/appengine-python-standard/blob/main/setup.py#L28)
|
|
34
34
|
"user-agents~=2.0",
|
|
35
35
|
"webob~=1.0",
|
|
36
36
|
]
|
|
@@ -52,6 +52,7 @@ classifiers = [
|
|
|
52
52
|
"Operating System :: OS Independent",
|
|
53
53
|
"Programming Language :: Python :: 3.12",
|
|
54
54
|
"Programming Language :: Python :: 3.13",
|
|
55
|
+
"Programming Language :: Python :: 3.14",
|
|
55
56
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
56
57
|
]
|
|
57
58
|
|
|
@@ -29,6 +29,7 @@ from .phone import PhoneBone
|
|
|
29
29
|
from .randomslice import RandomSliceBone
|
|
30
30
|
from .raw import RawBone
|
|
31
31
|
from .record import RecordBone
|
|
32
|
+
from .address import AddressBone, AddressRelSkel
|
|
32
33
|
from .relational import RelationalBone, RelationalConsistency, RelationalUpdateLevel
|
|
33
34
|
from .selectcountry import SelectCountryBone
|
|
34
35
|
from .select import (
|
|
@@ -49,6 +50,8 @@ from .user import UserBone
|
|
|
49
50
|
|
|
50
51
|
# Expose only specific names
|
|
51
52
|
__all = [
|
|
53
|
+
"AddressBone",
|
|
54
|
+
"AddressRelSkel",
|
|
52
55
|
"BaseBone",
|
|
53
56
|
"BooleanBone",
|
|
54
57
|
"CaptchaBone",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import typing as t
|
|
4
|
+
import urllib.parse
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .record import RecordBone
|
|
9
|
+
from .string import StringBone
|
|
10
|
+
from .selectcountry import SelectCountryBone
|
|
11
|
+
from .spatial import SpatialBone
|
|
12
|
+
from ..skeleton.relskel import RelSkel
|
|
13
|
+
from .. import db, i18n
|
|
14
|
+
from ..data.zip_patterns import ZIP_CODE_PATTERNS
|
|
15
|
+
|
|
16
|
+
CACHE_KIND = "viur-addressbone-geocache"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AddressRelSkel(RelSkel):
|
|
20
|
+
street_name = StringBone(descr="Street", required=True)
|
|
21
|
+
street_number = StringBone(descr="House number")
|
|
22
|
+
address_addition = StringBone(descr="Address addition")
|
|
23
|
+
zip_code = StringBone(
|
|
24
|
+
descr="ZIP / Postal code",
|
|
25
|
+
params={
|
|
26
|
+
"pattern": ZIP_CODE_PATTERNS,
|
|
27
|
+
"pattern-error": i18n.translate(
|
|
28
|
+
"viur.core.bones.address.zip_code.invalid",
|
|
29
|
+
defaultText="Invalid ZIP code",
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
city = StringBone(descr="City", required=True)
|
|
34
|
+
country = SelectCountryBone(descr="Country")
|
|
35
|
+
coordinates = SpatialBone(
|
|
36
|
+
descr="Coordinates",
|
|
37
|
+
boundsLat=(-90, 90),
|
|
38
|
+
boundsLng=(-180, 180),
|
|
39
|
+
gridDimensions=(10, 10),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AddressBone(RecordBone):
|
|
44
|
+
type = RecordBone.type + ".address"
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
using: t.Type[RelSkel] = AddressRelSkel,
|
|
50
|
+
format: str = "$(street_name) $(street_number), $(zip_code) $(city)",
|
|
51
|
+
**kwargs,
|
|
52
|
+
):
|
|
53
|
+
super().__init__(using=using, format=format, **kwargs)
|
|
54
|
+
|
|
55
|
+
def after_from_client(self, skel, name, errors):
|
|
56
|
+
value = skel[name]
|
|
57
|
+
if value is not None:
|
|
58
|
+
coords = self.geocode(value)
|
|
59
|
+
if coords:
|
|
60
|
+
value["coordinates"] = coords
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _cache_key(params: str) -> db.Key:
|
|
64
|
+
digest = hashlib.sha256(params.encode()).hexdigest()
|
|
65
|
+
return db.Key(CACHE_KIND, digest)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def geocode(skel: RelSkel) -> tuple[float, float] | None:
|
|
69
|
+
street = f"{skel['street_name'] or ''} {skel['street_number'] or ''}".strip()
|
|
70
|
+
params = {
|
|
71
|
+
"street": street,
|
|
72
|
+
"postalcode": skel["zip_code"] or "",
|
|
73
|
+
"city": skel["city"] or "",
|
|
74
|
+
"country": skel["country"] or "",
|
|
75
|
+
"format": "json",
|
|
76
|
+
"limit": 1,
|
|
77
|
+
}
|
|
78
|
+
cache_key = AddressBone._cache_key(urllib.parse.urlencode(params))
|
|
79
|
+
try:
|
|
80
|
+
cached = db.get(cache_key)
|
|
81
|
+
if cached is not None:
|
|
82
|
+
return cached["lat"], cached["lng"]
|
|
83
|
+
response = requests.get(
|
|
84
|
+
"https://nominatim.openstreetmap.org/search",
|
|
85
|
+
params=params,
|
|
86
|
+
headers={"User-Agent": "viur-addressbone/1.0 (viur.dev)"},
|
|
87
|
+
timeout=5,
|
|
88
|
+
)
|
|
89
|
+
if response.status_code != 200:
|
|
90
|
+
logging.error(
|
|
91
|
+
f"AddressBone: Nominatim returned {response.status_code=}"
|
|
92
|
+
)
|
|
93
|
+
return None
|
|
94
|
+
data = response.json()
|
|
95
|
+
if not data:
|
|
96
|
+
return None
|
|
97
|
+
lat, lng = float(data[0]["lat"]), float(data[0]["lon"])
|
|
98
|
+
entity = db.Entity(cache_key)
|
|
99
|
+
entity["lat"] = lat
|
|
100
|
+
entity["lng"] = lng
|
|
101
|
+
db.put(entity)
|
|
102
|
+
return lat, lng
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logging.error(f"AddressBone: Nominatim geocoding failed with {e=}")
|
|
105
|
+
return None
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from viur.core import conf, current
|
|
6
|
+
from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
|
|
7
|
+
|
|
8
|
+
from google.cloud import recaptchaenterprise_v1
|
|
9
|
+
from google.cloud.recaptchaenterprise_v1 import Assessment
|
|
10
|
+
|
|
11
|
+
if t.TYPE_CHECKING:
|
|
12
|
+
from viur.core.skeleton import SkeletonInstance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CaptchaBone(BaseBone):
|
|
16
|
+
r"""
|
|
17
|
+
The CaptchaBone validates reCAPTCHA Enterprise tokens to protect forms from bots.
|
|
18
|
+
|
|
19
|
+
It uses the Google reCAPTCHA Enterprise API and supports both invisible (v3-style score-based)
|
|
20
|
+
and visible (checkbox widget) challenges via the ``render_challenge`` parameter.
|
|
21
|
+
|
|
22
|
+
The token is submitted by the client as the bone's field value and verified server-side
|
|
23
|
+
against the configured site key. A configurable score threshold determines whether
|
|
24
|
+
invisible challenges pass.
|
|
25
|
+
|
|
26
|
+
.. seealso::
|
|
27
|
+
|
|
28
|
+
`Google reCAPTCHA Enterprise setup
|
|
29
|
+
<https://cloud.google.com/recaptcha/docs/set-up-non-google-cloud-environments-api-keys>`
|
|
30
|
+
for creating a site key and enabling the API.
|
|
31
|
+
|
|
32
|
+
Option :attr:`core.config.Security.captcha_default_public_key`
|
|
33
|
+
for global security settings.
|
|
34
|
+
|
|
35
|
+
Option :attr:`core.config.Security.captcha_enforce_always`
|
|
36
|
+
to enforce validation even on development servers.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
type = "captcha"
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
public_key: str = None,
|
|
45
|
+
score_threshold: float = 0.5,
|
|
46
|
+
render_challenge: bool = False,
|
|
47
|
+
recaptcha_action: str = "",
|
|
48
|
+
**kwargs: t.Any
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initializes a new CaptchaBone.
|
|
52
|
+
|
|
53
|
+
:param public_key: The reCAPTCHA Enterprise site key shown to the client.
|
|
54
|
+
Can be omitted if set globally via :attr:`core.config.Security.captcha_default_public_key`.
|
|
55
|
+
:param score_threshold: Minimum score (0–1) required for invisible challenges to pass.
|
|
56
|
+
Ignored when ``render_challenge`` is ``True``.
|
|
57
|
+
:param render_challenge: If ``True``, renders a visible checkbox widget instead of
|
|
58
|
+
running an invisible background check.
|
|
59
|
+
:param recaptcha_action: The action name passed to reCAPTCHA for analytics and scoring.
|
|
60
|
+
Should match the action used on the client side.
|
|
61
|
+
"""
|
|
62
|
+
if "publicKey" in kwargs:
|
|
63
|
+
warnings.warn("publicKey parameter is deprecated, please use public_key",
|
|
64
|
+
DeprecationWarning, stacklevel=2)
|
|
65
|
+
public_key = kwargs.pop("publicKey")
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
if not public_key and conf.security.captcha_default_public_key:
|
|
68
|
+
public_key = conf.security.captcha_default_public_key
|
|
69
|
+
if not public_key:
|
|
70
|
+
raise ValueError("CaptchaBone requires either a public_key or conf.security.captcha_default_public_key")
|
|
71
|
+
|
|
72
|
+
self.public_key = public_key
|
|
73
|
+
|
|
74
|
+
if not (0 < score_threshold <= 1):
|
|
75
|
+
raise ValueError("score_threshold must be between 0 and 1.")
|
|
76
|
+
self.render_challenge = render_challenge
|
|
77
|
+
self.recaptcha_action = recaptcha_action
|
|
78
|
+
self.score_threshold = score_threshold
|
|
79
|
+
self.required = True
|
|
80
|
+
|
|
81
|
+
def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Serializing the Captcha bone is not possible so it return False
|
|
84
|
+
"""
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def unserialize(self, skel: "SkeletonInstance", name) -> t.Literal[True]:
|
|
88
|
+
"""
|
|
89
|
+
Stores the public_key in the SkeletonInstance
|
|
90
|
+
|
|
91
|
+
:param skel: The target :class:`SkeletonInstance`.
|
|
92
|
+
:param name: The name of the CaptchaBone in the :class:`SkeletonInstance`.
|
|
93
|
+
|
|
94
|
+
:returns: boolean, that is true, as the Captcha bone is always unserialized successfully.
|
|
95
|
+
"""
|
|
96
|
+
skel.accessedValues[name] = self.public_key
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def fromClient(self, skel: "SkeletonInstance", name: str, data: dict) -> None | list[ReadFromClientError]:
|
|
100
|
+
"""
|
|
101
|
+
Load the reCAPTCHA token from the provided data and validate it with the help of the API.
|
|
102
|
+
|
|
103
|
+
reCAPTCHA provides the token via callback usually as "g-recaptcha-response",
|
|
104
|
+
but to fit into the skeleton logic, we support both names.
|
|
105
|
+
So the token can be provided as "g-recaptcha-response" or the name of the CaptchaBone in the Skeleton.
|
|
106
|
+
While the latter one is the preferred name.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
if not conf.security.captcha_enforce_always and conf.instance.is_dev_server:
|
|
110
|
+
logging.info("Skipping captcha validation on development server")
|
|
111
|
+
return None
|
|
112
|
+
if not conf.security.captcha_enforce_always and (user := current.user.get()) and "root" in user["access"]:
|
|
113
|
+
logging.info("Skipping captcha validation for root user")
|
|
114
|
+
return None # Don't bother trusted users with this (not supported by admin/vi anyway)
|
|
115
|
+
|
|
116
|
+
client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient()
|
|
117
|
+
|
|
118
|
+
# Set the attributes of the event to be tracked.
|
|
119
|
+
event = recaptchaenterprise_v1.Event()
|
|
120
|
+
event.site_key = self.public_key
|
|
121
|
+
if name in data:
|
|
122
|
+
event.token = data[name]
|
|
123
|
+
else:
|
|
124
|
+
return [ReadFromClientError(
|
|
125
|
+
ReadFromClientErrorSeverity.NotSet,
|
|
126
|
+
"Token not set"
|
|
127
|
+
)]
|
|
128
|
+
assessment = recaptchaenterprise_v1.Assessment()
|
|
129
|
+
assessment.event = event
|
|
130
|
+
|
|
131
|
+
project_name = f"projects/{conf.instance.project_id}"
|
|
132
|
+
|
|
133
|
+
# Create the assessment request.
|
|
134
|
+
request = recaptchaenterprise_v1.CreateAssessmentRequest()
|
|
135
|
+
request.assessment = assessment
|
|
136
|
+
request.parent = project_name
|
|
137
|
+
|
|
138
|
+
response = client.create_assessment(request)
|
|
139
|
+
|
|
140
|
+
if not response.token_properties.valid:
|
|
141
|
+
logging.info(
|
|
142
|
+
"The CreateAssessment call failed because the token was "
|
|
143
|
+
+ "invalid for the following reasons: "
|
|
144
|
+
+ str(response.token_properties.invalid_reason)
|
|
145
|
+
)
|
|
146
|
+
return [ReadFromClientError(
|
|
147
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
148
|
+
"Invalid Token"
|
|
149
|
+
)]
|
|
150
|
+
|
|
151
|
+
# Check if the expected action was executed.
|
|
152
|
+
if response.token_properties.action != self.recaptcha_action:
|
|
153
|
+
logging.info(
|
|
154
|
+
"The action attribute in your reCAPTCHA tag does not match the action you are expecting to score"
|
|
155
|
+
)
|
|
156
|
+
return [ReadFromClientError(
|
|
157
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
158
|
+
f"Invalid Action: {self.recaptcha_action}"
|
|
159
|
+
)]
|
|
160
|
+
else:
|
|
161
|
+
# Retrieve the risk score and reasons.
|
|
162
|
+
# For more information on interpreting the assessment, see:
|
|
163
|
+
# https://cloud.google.com/recaptcha/docs/interpret-assessment
|
|
164
|
+
if response.risk_analysis.score < self.score_threshold:
|
|
165
|
+
return [ReadFromClientError(
|
|
166
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
167
|
+
f"Invalid Captcha: {response.risk_analysis.score}"
|
|
168
|
+
)]
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def structure(self) -> dict:
|
|
173
|
+
return super().structure() | {
|
|
174
|
+
"public_key": self.public_key,
|
|
175
|
+
"render_challenge": self.render_challenge,
|
|
176
|
+
"action": self.recaptcha_action
|
|
177
|
+
}
|
|
@@ -3,6 +3,7 @@ import numbers
|
|
|
3
3
|
import sys
|
|
4
4
|
import typing as t
|
|
5
5
|
import warnings
|
|
6
|
+
import decimal as deci
|
|
6
7
|
|
|
7
8
|
from viur.core import db, i18n
|
|
8
9
|
from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
|
|
@@ -33,6 +34,7 @@ class NumericBone(BaseBone):
|
|
|
33
34
|
min: int | float = MIN,
|
|
34
35
|
max: int | float = MAX,
|
|
35
36
|
precision: int = 0,
|
|
37
|
+
decimal: bool = False,
|
|
36
38
|
mode=None, # deprecated!
|
|
37
39
|
**kwargs
|
|
38
40
|
):
|
|
@@ -42,6 +44,7 @@ class NumericBone(BaseBone):
|
|
|
42
44
|
:param min: Minimum accepted value (including).
|
|
43
45
|
:param max: Maximum accepted value (including).
|
|
44
46
|
:param precision: How may decimal places should be saved. Zero casts the value to int instead of float.
|
|
47
|
+
:param decimal: If True, use deci.Decimal internally for exact arithmetic.
|
|
45
48
|
"""
|
|
46
49
|
super().__init__(**kwargs)
|
|
47
50
|
|
|
@@ -61,6 +64,9 @@ class NumericBone(BaseBone):
|
|
|
61
64
|
self.precision = precision
|
|
62
65
|
self.min = min
|
|
63
66
|
self.max = max
|
|
67
|
+
self.decimal = decimal
|
|
68
|
+
if decimal:
|
|
69
|
+
self._quantize_exp = deci.Decimal(10) ** -precision
|
|
64
70
|
|
|
65
71
|
def __setattr__(self, key, value):
|
|
66
72
|
"""
|
|
@@ -80,16 +86,42 @@ class NumericBone(BaseBone):
|
|
|
80
86
|
|
|
81
87
|
return super().__setattr__(key, value)
|
|
82
88
|
|
|
89
|
+
def _convert_to_decimal(self, value) -> deci.Decimal | None:
|
|
90
|
+
"""Convert *value* to a quantized Decimal. Uses str() roundtrip for floats.
|
|
91
|
+
Accepts comma as decimal separator in strings."""
|
|
92
|
+
if value is None:
|
|
93
|
+
return None
|
|
94
|
+
with deci.localcontext() as ctx:
|
|
95
|
+
# +20 as buffer for integer digits to avoid InvalidOperation on quantize
|
|
96
|
+
ctx.prec = self.precision + 20
|
|
97
|
+
if isinstance(value, deci.Decimal):
|
|
98
|
+
return value.quantize(self._quantize_exp)
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
value = value.replace(",", ".", 1)
|
|
101
|
+
return deci.Decimal(value).quantize(self._quantize_exp)
|
|
102
|
+
if isinstance(value, (int, float)):
|
|
103
|
+
return deci.Decimal(str(value)).quantize(self._quantize_exp)
|
|
104
|
+
raise ValueError(f"Cannot convert {type(value).__name__} to Decimal")
|
|
105
|
+
|
|
83
106
|
def singleValueUnserialize(self, val):
|
|
84
107
|
if val is not None:
|
|
85
108
|
try:
|
|
109
|
+
if self.decimal:
|
|
110
|
+
if isinstance(val, dict) and "decimal" in val:
|
|
111
|
+
val = val["decimal"]
|
|
112
|
+
return self._convert_to_decimal(val)
|
|
86
113
|
return self._convert_to_numeric(val)
|
|
87
|
-
except (ValueError, TypeError):
|
|
114
|
+
except (ValueError, TypeError, deci.InvalidOperation):
|
|
88
115
|
return self.getDefaultValue(None) # FIXME: callable needs the skeleton instance
|
|
89
116
|
|
|
90
117
|
return val
|
|
91
118
|
|
|
92
119
|
def singleValueSerialize(self, value, skel: 'SkeletonInstance', name: str, parentIndexed: bool):
|
|
120
|
+
if self.decimal and value is not None:
|
|
121
|
+
return {
|
|
122
|
+
"val": self._convert_to_numeric(value),
|
|
123
|
+
"decimal": str(self._convert_to_decimal(value)),
|
|
124
|
+
}
|
|
93
125
|
return self.singleValueUnserialize(value) # same logic for unserialize here!
|
|
94
126
|
|
|
95
127
|
def isInvalid(self, value):
|
|
@@ -110,6 +142,8 @@ class NumericBone(BaseBone):
|
|
|
110
142
|
:return: Returns 0 for integers (when precision is 0) or 0.0 for floating-point numbers (when
|
|
111
143
|
precision is non-zero).
|
|
112
144
|
"""
|
|
145
|
+
if self.decimal:
|
|
146
|
+
return deci.Decimal(0).quantize(self._quantize_exp)
|
|
113
147
|
if self.precision:
|
|
114
148
|
return 0.0
|
|
115
149
|
else:
|
|
@@ -124,33 +158,53 @@ class NumericBone(BaseBone):
|
|
|
124
158
|
:param value: The raw value to be checked for emptiness.
|
|
125
159
|
:return: Returns True if the raw value is considered empty, otherwise False.
|
|
126
160
|
"""
|
|
161
|
+
if value is None:
|
|
162
|
+
return True
|
|
127
163
|
if isinstance(value, str) and not value:
|
|
128
164
|
return True
|
|
129
165
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
if self.decimal:
|
|
167
|
+
value = self._convert_to_decimal(value)
|
|
168
|
+
else:
|
|
169
|
+
value = self._convert_to_numeric(value)
|
|
170
|
+
except (ValueError, TypeError, deci.InvalidOperation):
|
|
132
171
|
return True
|
|
133
172
|
return value == self.getEmptyValue()
|
|
134
173
|
|
|
135
174
|
def singleValueFromClient(self, value, skel, bone_name, client_data):
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid)]
|
|
142
|
-
|
|
143
|
-
# Convert to float or int -- depending on the precision
|
|
144
|
-
# Since we convert direct to int if precision=0, a float value isn't valid
|
|
175
|
+
if self.decimal:
|
|
176
|
+
if value is None or (isinstance(value, str) and not value.strip()):
|
|
177
|
+
return self.getEmptyValue(), [
|
|
178
|
+
ReadFromClientError(ReadFromClientErrorSeverity.Empty, "No value entered")
|
|
179
|
+
]
|
|
145
180
|
try:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
181
|
+
if isinstance(value, str):
|
|
182
|
+
value = value.replace(",", ".", 1)
|
|
183
|
+
value = self._convert_to_decimal(value)
|
|
184
|
+
except (deci.InvalidOperation, ValueError, TypeError):
|
|
185
|
+
return self.getEmptyValue(), [
|
|
186
|
+
ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid decimal value")
|
|
187
|
+
]
|
|
149
188
|
|
|
150
|
-
if self.precision:
|
|
151
|
-
value = round(float(value), self.precision)
|
|
152
189
|
else:
|
|
153
|
-
|
|
190
|
+
if not isinstance(value, (int, float)):
|
|
191
|
+
# Replace , with .
|
|
192
|
+
try:
|
|
193
|
+
value = str(value).replace(",", ".", 1)
|
|
194
|
+
except TypeError:
|
|
195
|
+
return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid)]
|
|
196
|
+
|
|
197
|
+
# Convert to float or int -- depending on the precision
|
|
198
|
+
# Since we convert direct to int if precision=0, a float value isn't valid
|
|
199
|
+
try:
|
|
200
|
+
value = float(value) if self.precision else int(value)
|
|
201
|
+
except ValueError:
|
|
202
|
+
return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid)]
|
|
203
|
+
|
|
204
|
+
if self.precision:
|
|
205
|
+
value = round(float(value), self.precision)
|
|
206
|
+
else:
|
|
207
|
+
value = int(value)
|
|
154
208
|
|
|
155
209
|
# Check the limits after rounding, as the rounding may change the value.
|
|
156
210
|
if not (self.min <= value <= self.max):
|
|
@@ -182,23 +236,44 @@ class NumericBone(BaseBone):
|
|
|
182
236
|
prefix: t.Optional[str] = None
|
|
183
237
|
) -> db.Query:
|
|
184
238
|
updatedFilter = {}
|
|
185
|
-
|
|
186
239
|
for parmKey, paramValue in rawFilter.items():
|
|
187
240
|
if parmKey.startswith(name):
|
|
188
241
|
if parmKey != name and not parmKey.startswith(name + "$"):
|
|
189
242
|
# It's just another bone which name start's with our's
|
|
190
243
|
continue
|
|
191
244
|
try:
|
|
192
|
-
if
|
|
245
|
+
if self.decimal:
|
|
246
|
+
paramValue = float(str(paramValue).replace(",", ".", 1))
|
|
247
|
+
elif not self.precision:
|
|
193
248
|
paramValue = int(paramValue)
|
|
194
249
|
else:
|
|
195
250
|
paramValue = float(paramValue)
|
|
196
|
-
except ValueError:
|
|
251
|
+
except (ValueError, deci.InvalidOperation):
|
|
197
252
|
# The value we should filter by is garbage, cancel this query
|
|
198
253
|
logging.warning(f"Invalid filtering! Unparsable int/float supplied to NumericBone {name}")
|
|
199
254
|
raise RuntimeError()
|
|
200
255
|
updatedFilter[parmKey] = paramValue
|
|
201
256
|
|
|
257
|
+
if self.decimal:
|
|
258
|
+
# Values are stored as {"val": float, "decimal": str} — filter on the .val sub-property
|
|
259
|
+
prop = (prefix or "") + name + ".val"
|
|
260
|
+
for key, value in updatedFilter.items():
|
|
261
|
+
if key == name:
|
|
262
|
+
dbFilter.filter(prop + " =", value)
|
|
263
|
+
else:
|
|
264
|
+
op = key[len(name) + 1:] # the part after "$"
|
|
265
|
+
if op == "lt":
|
|
266
|
+
dbFilter.filter(prop + " <", value)
|
|
267
|
+
elif op == "le":
|
|
268
|
+
dbFilter.filter(prop + " <=", value)
|
|
269
|
+
elif op == "gt":
|
|
270
|
+
dbFilter.filter(prop + " >", value)
|
|
271
|
+
elif op == "ge":
|
|
272
|
+
dbFilter.filter(prop + " >=", value)
|
|
273
|
+
else:
|
|
274
|
+
dbFilter.filter(prop + " =", value)
|
|
275
|
+
return dbFilter
|
|
276
|
+
|
|
202
277
|
return super().buildDBFilter(name, skel, dbFilter, updatedFilter, prefix)
|
|
203
278
|
|
|
204
279
|
def getSearchTags(self, skel: "SkeletonInstance", name: str) -> set[str]:
|
|
@@ -240,9 +315,12 @@ class NumericBone(BaseBone):
|
|
|
240
315
|
"""
|
|
241
316
|
super().refresh(skel, boneName)
|
|
242
317
|
|
|
243
|
-
def refresh_single_value(value: t.Any) -> float | int:
|
|
318
|
+
def refresh_single_value(value: t.Any) -> float | int | deci.Decimal:
|
|
244
319
|
if value == "":
|
|
245
320
|
return self.getEmptyValue()
|
|
321
|
+
elif self.decimal:
|
|
322
|
+
if not isinstance(value, (deci.Decimal, type(None))):
|
|
323
|
+
return self._convert_to_decimal(value)
|
|
246
324
|
elif not isinstance(value, (int, float, type(None))):
|
|
247
325
|
return self._convert_to_numeric(value)
|
|
248
326
|
return value
|
|
@@ -276,4 +354,6 @@ class NumericBone(BaseBone):
|
|
|
276
354
|
"min": self.min,
|
|
277
355
|
"max": self.max,
|
|
278
356
|
"precision": self.precision,
|
|
357
|
+
"decimal": self.decimal,
|
|
358
|
+
|
|
279
359
|
}
|
|
@@ -112,13 +112,13 @@ class RandomSliceBone(BaseBone):
|
|
|
112
112
|
q = db.QueryDefinition(origKind, {}, [])
|
|
113
113
|
property, value = applyFilterHook(dbFilter, f"{name} <=", rndVal)
|
|
114
114
|
q.filters[property] = value
|
|
115
|
-
q.orders = [(name, db.SortOrder.Descending)]
|
|
115
|
+
q.orders = [db.QueryOrder(name, db.SortOrder.Descending)]
|
|
116
116
|
queries.append(q)
|
|
117
117
|
# Left Side
|
|
118
118
|
q = db.QueryDefinition(origKind, {}, [])
|
|
119
119
|
property, value = applyFilterHook(dbFilter, f"{name} >", rndVal)
|
|
120
120
|
q.filters[property] = value
|
|
121
|
-
q.orders = [(name
|
|
121
|
+
q.orders = [db.QueryOrder(name)]
|
|
122
122
|
queries.append(q)
|
|
123
123
|
dbFilter.queries = queries
|
|
124
124
|
# Map the original filter back in
|