viur-core 3.9.0.dev5__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.dev5/src/viur_core.egg-info → viur_core-3.9.0.dev6}/PKG-INFO +8 -8
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/pyproject.toml +7 -7
- {viur_core-3.9.0.dev5 → 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.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/numeric.py +102 -22
- viur_core-3.9.0.dev6/src/viur/core/data/zip_patterns.py +257 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/query.py +97 -52
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/transport.py +36 -8
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/types.py +5 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/utils.py +14 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/module.py +3 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/history.py +42 -13
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/skelmodule.py +4 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/json/default.py +5 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/instance.py +11 -2
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/tasks.py +3 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6/src/viur_core.egg-info}/PKG-INFO +8 -8
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/SOURCES.txt +3 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/requires.txt +6 -7
- 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.dev5/tests/test_db.py +0 -81
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/LICENSE +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/README.md +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/setup.cfg +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/base.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/boolean.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/captcha.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/code.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/email.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/randomslice.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/record.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/relational.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/spatial.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/string.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/text.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/cache.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/config.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/contrib/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/contrib/loginkey.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/contrib/ratelimit.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/cache.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/email.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/file.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/script.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/translation.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/modules/user.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/render/vi/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/request.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/securityheaders.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/session.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/meta.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/skeleton.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/tasks.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/skeleton/utils.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/utils/__init__.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/src/viur_core.egg-info/top_level.txt +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/tests/test_config.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/tests/test_decorators.py +0 -0
- {viur_core-3.9.0.dev5 → viur_core-3.9.0.dev6}/tests/test_errors.py +0 -0
- {viur_core-3.9.0.dev5 → 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
|
|
@@ -50,22 +51,21 @@ Requires-Dist: google-cloud-iam~=2.0
|
|
|
50
51
|
Requires-Dist: google-cloud-logging~=3.0
|
|
51
52
|
Requires-Dist: google-cloud-recaptcha-enterprise~=1.0
|
|
52
53
|
Requires-Dist: google-cloud-secret-manager~=2.0
|
|
53
|
-
Requires-Dist: google-cloud-storage
|
|
54
|
+
Requires-Dist: google-cloud-storage<4,>=2
|
|
54
55
|
Requires-Dist: google-cloud-tasks~=2.0
|
|
55
56
|
Requires-Dist: google-resumable-media~=2.0
|
|
56
57
|
Requires-Dist: googleapis-common-protos[grpc]~=1.0
|
|
57
|
-
Requires-Dist: gunicorn
|
|
58
|
+
Requires-Dist: gunicorn<27,>=23
|
|
58
59
|
Requires-Dist: jinja2~=3.0
|
|
59
60
|
Requires-Dist: jsonschema~=4.0
|
|
60
61
|
Requires-Dist: logics-py==0.0.12
|
|
61
|
-
Requires-Dist: pillow
|
|
62
|
+
Requires-Dist: pillow<13,>=11
|
|
62
63
|
Requires-Dist: pyotp~=2.0
|
|
63
|
-
Requires-Dist: pytz
|
|
64
|
+
Requires-Dist: pytz>=2023
|
|
64
65
|
Requires-Dist: pyyaml~=6.0
|
|
65
|
-
Requires-Dist: qrcode
|
|
66
|
+
Requires-Dist: qrcode<9,>=7
|
|
66
67
|
Requires-Dist: requests~=2.0
|
|
67
68
|
Requires-Dist: tzlocal~=5.0
|
|
68
|
-
Requires-Dist: urllib3==1.26.20
|
|
69
69
|
Requires-Dist: user-agents~=2.0
|
|
70
70
|
Requires-Dist: webob~=1.0
|
|
71
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",
|
|
@@ -16,22 +16,21 @@ dependencies = [
|
|
|
16
16
|
"google-cloud-logging~=3.0",
|
|
17
17
|
"google-cloud-recaptcha-enterprise~=1.0",
|
|
18
18
|
"google-cloud-secret-manager~=2.0",
|
|
19
|
-
"google-cloud-storage
|
|
19
|
+
"google-cloud-storage>=2,<4",
|
|
20
20
|
"google-cloud-tasks~=2.0",
|
|
21
21
|
"google-resumable-media~=2.0",
|
|
22
22
|
"googleapis-common-protos[grpc]~=1.0",
|
|
23
|
-
"gunicorn
|
|
23
|
+
"gunicorn>=23,<27",
|
|
24
24
|
"jinja2~=3.0",
|
|
25
25
|
"jsonschema~=4.0",
|
|
26
26
|
"logics-py==0.0.12",
|
|
27
|
-
"pillow
|
|
27
|
+
"pillow>=11,<13",
|
|
28
28
|
"pyotp~=2.0",
|
|
29
|
-
"pytz
|
|
29
|
+
"pytz>=2023",
|
|
30
30
|
"pyyaml~=6.0",
|
|
31
|
-
"qrcode
|
|
31
|
+
"qrcode>=7,<9",
|
|
32
32
|
"requests~=2.0",
|
|
33
33
|
"tzlocal~=5.0",
|
|
34
|
-
"urllib3==1.26.20", # for appengine-python-standard (https://github.com/GoogleCloudPlatform/appengine-python-standard/blob/main/setup.py#L28)
|
|
35
34
|
"user-agents~=2.0",
|
|
36
35
|
"webob~=1.0",
|
|
37
36
|
]
|
|
@@ -53,6 +52,7 @@ classifiers = [
|
|
|
53
52
|
"Operating System :: OS Independent",
|
|
54
53
|
"Programming Language :: Python :: 3.12",
|
|
55
54
|
"Programming Language :: Python :: 3.13",
|
|
55
|
+
"Programming Language :: Python :: 3.14",
|
|
56
56
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
57
57
|
]
|
|
58
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
|
|
@@ -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
|
}
|