spinta 0.2.dev19__py3-none-any.whl → 0.2.dev20__py3-none-any.whl
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.
- spinta/adapters/rc/__init__.py +1 -0
- spinta/adapters/rc/signature_adapter.py +118 -0
- spinta/adapters/soap_plugins.py +122 -0
- spinta/cli/config.py +13 -7
- spinta/commands/read.py +2 -3
- spinta/components.py +1 -0
- spinta/config.py +1 -1
- spinta/config.yml +3 -0
- spinta/core/access.py +2 -1
- spinta/core/ufuncs.py +5 -1
- spinta/datasets/backends/dataframe/backends/soap/ufuncs/ufuncs.py +108 -15
- spinta/datasets/backends/dataframe/commands/check.py +33 -0
- spinta/datasets/backends/dataframe/ufuncs/query/components.py +10 -0
- spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py +62 -11
- spinta/datasets/commands/link.py +4 -2
- spinta/exceptions.py +5 -3
- spinta/manifests/components.py +1 -1
- spinta/manifests/dict/components.py +20 -14
- spinta/manifests/dict/helpers.py +296 -48
- spinta/manifests/helpers.py +1 -1
- spinta/manifests/xsd/components.py +1 -1
- spinta/manifests/xsd/helpers.py +1076 -946
- spinta/testing/client.py +2 -0
- spinta/types/config.py +9 -0
- spinta/types/model.py +33 -38
- spinta/ufuncs/querybuilder/ufuncs.py +1 -0
- {spinta-0.2.dev19.dist-info → spinta-0.2.dev20.dist-info}/METADATA +2 -2
- {spinta-0.2.dev19.dist-info → spinta-0.2.dev20.dist-info}/RECORD +32 -33
- spinta/manifests/xsd2/commands/__init__.py +0 -0
- spinta/manifests/xsd2/commands/configure.py +0 -15
- spinta/manifests/xsd2/commands/load.py +0 -60
- spinta/manifests/xsd2/components.py +0 -9
- spinta/manifests/xsd2/helpers.py +0 -1310
- /spinta/{manifests/xsd2 → adapters}/__init__.py +0 -0
- {spinta-0.2.dev19.dist-info → spinta-0.2.dev20.dist-info}/WHEEL +0 -0
- {spinta-0.2.dev19.dist-info → spinta-0.2.dev20.dist-info}/entry_points.txt +0 -0
- {spinta-0.2.dev19.dist-info → spinta-0.2.dev20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Organization-specific SOAP helpers (e.g. RC broker request signing)."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from lxml import etree
|
|
5
|
+
|
|
6
|
+
from spinta.components import Context
|
|
7
|
+
from spinta.core.config import RawConfig
|
|
8
|
+
from spinta.core.ufuncs import Expr
|
|
9
|
+
from spinta.datasets.backends.dataframe.backends.soap.ufuncs.components import SoapQueryBuilder
|
|
10
|
+
from spinta.datasets.backends.dataframe.backends.soap.ufuncs.ufuncs import MakeCDATA
|
|
11
|
+
|
|
12
|
+
RC_ACTION_TYPE_FIELD = "ActionType"
|
|
13
|
+
RC_CALLER_CODE_FIELD = "CallerCode"
|
|
14
|
+
RC_END_USER_INFO_FIELD = "EndUserInfo"
|
|
15
|
+
RC_PARAMETERS_FIELD = "Parameters"
|
|
16
|
+
RC_TIME_FIELD = "Time"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_quoted(value: str) -> bool:
|
|
20
|
+
"""True if ``value`` has matching outer ``'`` or ``"`` quotes (Katalogas ``is_quoted``)."""
|
|
21
|
+
return len(value) > 1 and value[0] == value[-1] and value[0] in ('"', "'")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _require_rc_private_key_path(raw_config: RawConfig | None) -> str:
|
|
25
|
+
"""Return the configured PEM path, or raise if raw config / key path is missing."""
|
|
26
|
+
if raw_config is None:
|
|
27
|
+
raise RuntimeError("RC signature adapter was loaded but application configuration is missing.")
|
|
28
|
+
path = raw_config.get("rc_signature", "private_key_path", default=None)
|
|
29
|
+
if not path or not str(path).strip():
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"When using the RC signature SOAP adapter, set `rc_signature.private_key_path` in configuration."
|
|
32
|
+
)
|
|
33
|
+
return str(path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_soap_adapter_config(raw_config: RawConfig | None) -> None:
|
|
37
|
+
"""Invoked by soap plugin loader when this module is listed in ``soap_adapter_modules``."""
|
|
38
|
+
_require_rc_private_key_path(raw_config)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_private_key_path(context: Context) -> str:
|
|
42
|
+
return _require_rc_private_key_path(context.get("rc"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _compute_rc_signature(args: str, key_path: str) -> str:
|
|
46
|
+
"""Compute RC-style signature using openssl CLI.
|
|
47
|
+
This mirrors the legacy shell example:
|
|
48
|
+
echo -n "$ARGS" | openssl dgst -sha256 -sign $PRIVATE_KEY_FILE | base64 -w0
|
|
49
|
+
"""
|
|
50
|
+
proc = subprocess.run(
|
|
51
|
+
["openssl", "dgst", "-sha256", "-sign", key_path],
|
|
52
|
+
input=args.encode("utf-8"),
|
|
53
|
+
stdout=subprocess.PIPE,
|
|
54
|
+
stderr=subprocess.PIPE,
|
|
55
|
+
check=True,
|
|
56
|
+
)
|
|
57
|
+
sig_b64 = base64.b64encode(proc.stdout).decode("ascii")
|
|
58
|
+
|
|
59
|
+
return sig_b64.replace("\r", "").replace("\n", "")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_rc_string_to_sign(soap_body: dict[str, object]) -> str:
|
|
63
|
+
"""Return the plaintext that RC signs and verifies for SOAP calls.
|
|
64
|
+
|
|
65
|
+
The broker does not sign the whole XML; it expects an RSA-SHA256 signature over one
|
|
66
|
+
concatenated string, in this exact order with no delimiters: ActionType, CallerCode,
|
|
67
|
+
EndUserInfo, Parameters, Time. The server rebuilds the same string and checks the
|
|
68
|
+
signature, so we must assemble identical pieces from `soap_body`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
input_block = soap_body.get("input")
|
|
72
|
+
input_dict = input_block if isinstance(input_block, dict) else None
|
|
73
|
+
|
|
74
|
+
def _get(name: str, default: str = "") -> str:
|
|
75
|
+
value = soap_body.get(f"input/{name}")
|
|
76
|
+
if value is None and input_dict is not None:
|
|
77
|
+
value = input_dict.get(name)
|
|
78
|
+
|
|
79
|
+
if value is None:
|
|
80
|
+
value = soap_body.get(name, default)
|
|
81
|
+
|
|
82
|
+
if isinstance(value, MakeCDATA):
|
|
83
|
+
value = value.data
|
|
84
|
+
|
|
85
|
+
if isinstance(value, etree.CDATA):
|
|
86
|
+
value = str(value)
|
|
87
|
+
|
|
88
|
+
if name == RC_PARAMETERS_FIELD and isinstance(value, str) and is_quoted(value):
|
|
89
|
+
value = value[1:-1]
|
|
90
|
+
|
|
91
|
+
return "" if value is None else str(value)
|
|
92
|
+
|
|
93
|
+
action_type = _get(RC_ACTION_TYPE_FIELD)
|
|
94
|
+
caller_code = _get(RC_CALLER_CODE_FIELD)
|
|
95
|
+
end_user_info = _get(RC_END_USER_INFO_FIELD, "")
|
|
96
|
+
parameters = _get(RC_PARAMETERS_FIELD, "")
|
|
97
|
+
time_value = _get(RC_TIME_FIELD)
|
|
98
|
+
|
|
99
|
+
return f"{action_type}{caller_code}{end_user_info}{parameters}{time_value}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def compute_rc_signature_from_body(soap_body: dict[str, object], context: Context) -> str:
|
|
103
|
+
"""Build string-to-sign from soap_body and return base64 signature."""
|
|
104
|
+
key_path = _get_private_key_path(context)
|
|
105
|
+
args = build_rc_string_to_sign(soap_body)
|
|
106
|
+
|
|
107
|
+
return _compute_rc_signature(args, key_path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_deferred_prepare_names() -> list[str]:
|
|
111
|
+
return ["rc_signature"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_body_resolvers() -> dict[str, object]:
|
|
115
|
+
def rc_signature_resolver(env: SoapQueryBuilder, expr: Expr | None = None) -> str:
|
|
116
|
+
return compute_rc_signature_from_body(env.soap_request_body, env.context)
|
|
117
|
+
|
|
118
|
+
return {"rc_signature": rc_signature_resolver}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Load optional SOAP adapter modules from ``soap_adapter_modules`` paths in config."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from spinta.core.ufuncs import Expr, UFuncRegistry
|
|
10
|
+
from spinta.core.config import RawConfig
|
|
11
|
+
from spinta.ufuncs.loadbuilder.components import LoadBuilder
|
|
12
|
+
from spinta.datasets.backends.dataframe.backends.soap.ufuncs.components import SoapQueryBuilder
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_deferred_prepare_names_cache: set[str] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_deferred_prepare_names() -> set[str]:
|
|
21
|
+
"""Names of prepare ufuncs left unresolved until the SOAP body is built (empty before register)."""
|
|
22
|
+
return _deferred_prepare_names_cache or set()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_adapter_module_from_path(file_path: str, raw_config: RawConfig | None = None) -> ModuleType | None:
|
|
26
|
+
try:
|
|
27
|
+
path = Path(file_path)
|
|
28
|
+
if not path.exists():
|
|
29
|
+
log.warning("SOAP adapter module not found: %s", file_path)
|
|
30
|
+
return None
|
|
31
|
+
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
32
|
+
if spec is None or spec.loader is None:
|
|
33
|
+
log.warning("Failed to create module spec for: %s", file_path)
|
|
34
|
+
return None
|
|
35
|
+
module = importlib.util.module_from_spec(spec)
|
|
36
|
+
spec.loader.exec_module(module)
|
|
37
|
+
log.info("Loaded SOAP adapter module from: %s", file_path)
|
|
38
|
+
if hasattr(module, "validate_soap_adapter_config"):
|
|
39
|
+
module.validate_soap_adapter_config(raw_config)
|
|
40
|
+
return module
|
|
41
|
+
except Exception as e:
|
|
42
|
+
log.warning("Failed to load SOAP adapter module %s: %s", file_path, e)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_adapters_from_config(raw_config: RawConfig | None) -> tuple[set[str], dict[str, Callable[..., Any]]]:
|
|
47
|
+
deferred: set[str] = set()
|
|
48
|
+
body_resolvers: dict[str, Callable[..., Any]] = {}
|
|
49
|
+
|
|
50
|
+
if raw_config is None:
|
|
51
|
+
return deferred, body_resolvers
|
|
52
|
+
|
|
53
|
+
module_paths = raw_config.get("soap_adapter_modules", default=[])
|
|
54
|
+
if not module_paths:
|
|
55
|
+
return deferred, body_resolvers
|
|
56
|
+
|
|
57
|
+
if isinstance(module_paths, str):
|
|
58
|
+
module_paths = [module_paths]
|
|
59
|
+
|
|
60
|
+
for module_path in module_paths:
|
|
61
|
+
module = _load_adapter_module_from_path(module_path, raw_config)
|
|
62
|
+
if module is None:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if hasattr(module, "get_deferred_prepare_names"):
|
|
66
|
+
try:
|
|
67
|
+
result = module.get_deferred_prepare_names()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
log.warning("Failed to call get_deferred_prepare_names in %s: %s", module_path, e)
|
|
70
|
+
else:
|
|
71
|
+
if isinstance(result, (list, tuple)):
|
|
72
|
+
deferred.update(result)
|
|
73
|
+
else:
|
|
74
|
+
deferred.add(str(result))
|
|
75
|
+
|
|
76
|
+
if hasattr(module, "get_body_resolvers"):
|
|
77
|
+
try:
|
|
78
|
+
result = module.get_body_resolvers()
|
|
79
|
+
except Exception as e:
|
|
80
|
+
log.warning("Failed to call get_body_resolvers in %s: %s", module_path, e)
|
|
81
|
+
else:
|
|
82
|
+
if isinstance(result, dict):
|
|
83
|
+
body_resolvers.update(result)
|
|
84
|
+
|
|
85
|
+
return deferred, body_resolvers
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _deferred_prepare_resolver(env, expr: Expr) -> Expr:
|
|
89
|
+
env.param.soap_body = {env.this: expr}
|
|
90
|
+
return expr
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _make_body_resolver(callable_fn: Callable[..., Any]) -> Callable[[SoapQueryBuilder, Expr], Any]:
|
|
94
|
+
"""Adapter may implement ``fn(env)`` only; fall back if ``fn(env, expr)`` raises ``TypeError``."""
|
|
95
|
+
|
|
96
|
+
def body_resolver(env: SoapQueryBuilder, expr: Expr) -> Any:
|
|
97
|
+
try:
|
|
98
|
+
return callable_fn(env, expr)
|
|
99
|
+
except TypeError:
|
|
100
|
+
return callable_fn(env)
|
|
101
|
+
|
|
102
|
+
return body_resolver
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def register_soap_ufuncs(registry: UFuncRegistry, raw_config: RawConfig | None = None) -> None:
|
|
106
|
+
"""Register SOAP adapter resolvers from config (`soap_adapter_modules`).
|
|
107
|
+
Adapters are loaded from local Python files listed in:
|
|
108
|
+
soap_adapter_modules:
|
|
109
|
+
- /path/to/adapter.py
|
|
110
|
+
Call this after resolver.collect().
|
|
111
|
+
"""
|
|
112
|
+
global _deferred_prepare_names_cache
|
|
113
|
+
|
|
114
|
+
deferred, body_resolvers = _load_adapters_from_config(raw_config)
|
|
115
|
+
|
|
116
|
+
_deferred_prepare_names_cache = deferred
|
|
117
|
+
|
|
118
|
+
for name in deferred:
|
|
119
|
+
registry.register(name, _deferred_prepare_resolver, (LoadBuilder, Expr))
|
|
120
|
+
|
|
121
|
+
for name, callable_fn in body_resolvers.items():
|
|
122
|
+
registry.register(name, _make_body_resolver(callable_fn), (SoapQueryBuilder, Expr))
|
spinta/cli/config.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from typing import Optional
|
|
1
|
+
from typing import List, Optional
|
|
3
2
|
|
|
4
|
-
from typer import Argument
|
|
3
|
+
from typer import Argument, Option, echo
|
|
5
4
|
from typer import Context as TyperContext
|
|
6
|
-
from typer import Option
|
|
7
|
-
from typer import echo
|
|
8
5
|
|
|
6
|
+
from spinta import commands
|
|
9
7
|
from spinta.cli.helpers.manifest import convert_str_to_manifest_path
|
|
10
8
|
from spinta.cli.helpers.store import prepare_manifest
|
|
11
|
-
from spinta.
|
|
9
|
+
from spinta.components import Context, Store
|
|
12
10
|
from spinta.core.config import KeyFormat
|
|
13
11
|
from spinta.core.context import configure_context
|
|
12
|
+
from spinta.core.enums import Mode
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def config(
|
|
@@ -32,9 +31,16 @@ def check(
|
|
|
32
31
|
):
|
|
33
32
|
"""Check configuration and manifests"""
|
|
34
33
|
manifests = convert_str_to_manifest_path(manifests)
|
|
35
|
-
context = configure_context(ctx.obj, manifests, mode=mode, check_names=check_names)
|
|
34
|
+
context: Context = configure_context(ctx.obj, manifests, mode=mode, check_names=check_names)
|
|
36
35
|
prepare_manifest(context, ensure_config_dir=True, full_load=True)
|
|
37
36
|
manager = context.get("error_manager")
|
|
37
|
+
|
|
38
|
+
store: Store = context.get("store")
|
|
39
|
+
if store.manifest:
|
|
40
|
+
for model in commands.get_models(context, store.manifest).values():
|
|
41
|
+
if model.external and model.external.resource and model.external.resource.backend:
|
|
42
|
+
commands.check(context, model, model.external.resource.backend)
|
|
43
|
+
|
|
38
44
|
handler = manager.handler
|
|
39
45
|
|
|
40
46
|
if handler.get_counts():
|
spinta/commands/read.py
CHANGED
|
@@ -123,11 +123,10 @@ def getall(context: Context, model: Model, page: Page, *, query: Expr = None, li
|
|
|
123
123
|
page.size = size + 1
|
|
124
124
|
|
|
125
125
|
page_meta = PaginationMetaData(page_size=size, limit=limit)
|
|
126
|
+
paginated_query = add_page_expr(query, page)
|
|
126
127
|
while not page_meta.is_finished:
|
|
127
128
|
page_meta.is_finished = True
|
|
128
|
-
|
|
129
|
-
rows = commands.getall(context, model, backend, query=query, **kwargs)
|
|
130
|
-
|
|
129
|
+
rows = commands.getall(context, model, backend, query=paginated_query, **kwargs)
|
|
131
130
|
yield from get_paginated_values(page, page_meta, rows, extract_source_page_keys)
|
|
132
131
|
|
|
133
132
|
|
spinta/components.py
CHANGED
|
@@ -1095,6 +1095,7 @@ class Config:
|
|
|
1095
1095
|
scope_log: bool
|
|
1096
1096
|
check_contract_scopes: bool
|
|
1097
1097
|
default_auth_client: str
|
|
1098
|
+
default_access_level: str
|
|
1098
1099
|
http_basic_auth: bool
|
|
1099
1100
|
token_validation_key: dict | None = None
|
|
1100
1101
|
token_validation_keys_download_url: str | None = None
|
spinta/config.py
CHANGED
|
@@ -54,7 +54,6 @@ CONFIG = {
|
|
|
54
54
|
"xml": "spinta.manifests.dict.components:XmlManifest",
|
|
55
55
|
"internal": "spinta.manifests.internal_sql.components:InternalSQLManifest",
|
|
56
56
|
"xsd": "spinta.manifests.xsd.components:XsdManifest",
|
|
57
|
-
"xsd2": "spinta.manifests.xsd2.components:XsdManifest2",
|
|
58
57
|
"openapi": "spinta.manifests.open_api.components:OpenAPIManifest",
|
|
59
58
|
},
|
|
60
59
|
"backends": {
|
|
@@ -347,6 +346,7 @@ CONFIG = {
|
|
|
347
346
|
},
|
|
348
347
|
"config_path": pathlib.Path("tests/config"),
|
|
349
348
|
"default_auth_client": "baa448a8-205c-4faa-a048-a10e4b32a136",
|
|
349
|
+
"default_access_level": "protected",
|
|
350
350
|
"sync_retry_count": 0,
|
|
351
351
|
},
|
|
352
352
|
},
|
spinta/config.yml
CHANGED
spinta/core/access.py
CHANGED
|
@@ -73,6 +73,7 @@ def link_access_param(
|
|
|
73
73
|
] = (),
|
|
74
74
|
*,
|
|
75
75
|
use_given: bool = True,
|
|
76
|
+
default_access: Access = Access.private,
|
|
76
77
|
) -> None:
|
|
77
78
|
if component.access is None:
|
|
78
79
|
for parent in parents:
|
|
@@ -83,4 +84,4 @@ def link_access_param(
|
|
|
83
84
|
component.access = candidate
|
|
84
85
|
break
|
|
85
86
|
else:
|
|
86
|
-
component.access =
|
|
87
|
+
component.access = default_access
|
spinta/core/ufuncs.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import functools
|
|
5
5
|
import importlib
|
|
6
|
-
from typing import Any, Optional, List, Union
|
|
6
|
+
from typing import Any, Optional, List, Union, Callable
|
|
7
7
|
from typing import Dict
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
from typing import Tuple
|
|
@@ -140,6 +140,10 @@ class UFuncRegistry:
|
|
|
140
140
|
# automatically by __call__.
|
|
141
141
|
importlib.import_module(module)
|
|
142
142
|
|
|
143
|
+
def register(self, name: str, func: Callable, types: tuple) -> None:
|
|
144
|
+
"""Register a single (name, func, types) for plugin-discovered resolvers."""
|
|
145
|
+
self._ufuncs.append((name, func, types))
|
|
146
|
+
|
|
143
147
|
def ufuncs(self):
|
|
144
148
|
# Create commands from collected ufuncs.
|
|
145
149
|
ufuncs = {}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
from lxml import etree
|
|
6
7
|
|
|
8
|
+
from spinta.adapters.soap_plugins import get_deferred_prepare_names
|
|
7
9
|
from spinta.auth import authorized, query_client
|
|
8
10
|
from spinta.components import Property
|
|
9
11
|
from spinta.core.enums import Action
|
|
@@ -21,6 +23,10 @@ from spinta.utils.config import get_clients_path
|
|
|
21
23
|
from spinta.utils.data import take
|
|
22
24
|
from spinta.utils.schema import NA
|
|
23
25
|
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
SOAP_BODY_VALUE_TYPE_CDATA = "cdata"
|
|
29
|
+
|
|
24
30
|
|
|
25
31
|
class MakeCDATA:
|
|
26
32
|
"""
|
|
@@ -47,7 +53,7 @@ def eq_(env: SoapQueryBuilder, field: Bind, value: object) -> Expr | None:
|
|
|
47
53
|
try:
|
|
48
54
|
prop = env.resolve_property(field)
|
|
49
55
|
except PropertyNotFound:
|
|
50
|
-
# leave query parameter for other
|
|
56
|
+
# leave query parameter for other resolvers
|
|
51
57
|
return Expr("eq", field, value)
|
|
52
58
|
|
|
53
59
|
if not isinstance(prop.external.prepare, Expr):
|
|
@@ -59,7 +65,7 @@ def eq_(env: SoapQueryBuilder, field: Bind, value: object) -> Expr | None:
|
|
|
59
65
|
@ufunc.resolver(SoapQueryBuilder, Expr, name="and")
|
|
60
66
|
def and_(env: SoapQueryBuilder, expr: Expr) -> list[Any]:
|
|
61
67
|
args, kwargs = expr.resolve(env)
|
|
62
|
-
args = [
|
|
68
|
+
args = [arg_item for arg_item in args if arg_item is not None]
|
|
63
69
|
return env.call("and", args)
|
|
64
70
|
|
|
65
71
|
|
|
@@ -72,16 +78,88 @@ def and_(env: SoapQueryBuilder, args: list) -> Any:
|
|
|
72
78
|
|
|
73
79
|
|
|
74
80
|
def _finalize_soap_request_body_resolve(env: SoapQueryBuilder) -> None:
|
|
81
|
+
deferred_names = get_deferred_prepare_names()
|
|
82
|
+
|
|
75
83
|
for param_body_key, param_body_value in env.soap_request_body.items():
|
|
76
84
|
if not isinstance(param_body_value, Expr):
|
|
77
85
|
continue
|
|
86
|
+
|
|
87
|
+
if getattr(param_body_value, "name", None) in deferred_names:
|
|
88
|
+
continue
|
|
89
|
+
|
|
78
90
|
env.soap_request_body[param_body_key] = env.resolve(param_body_value)
|
|
79
91
|
|
|
80
92
|
|
|
93
|
+
def _param_for_soap_body_key(env: SoapQueryBuilder, param_source: str, expr: Expr | None = None) -> Param | None:
|
|
94
|
+
"""Find the resource Param that owns this SOAP body slot (flat key like ``input/Signature``)."""
|
|
95
|
+
param_lower = param_source.lower()
|
|
96
|
+
|
|
97
|
+
for resource_param in env.params.values():
|
|
98
|
+
soap_body = getattr(resource_param, "soap_body", None)
|
|
99
|
+
if not soap_body:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if param_source in soap_body:
|
|
103
|
+
return resource_param
|
|
104
|
+
|
|
105
|
+
if any(soap_key.lower() == param_lower for soap_key in soap_body):
|
|
106
|
+
return resource_param
|
|
107
|
+
|
|
108
|
+
if expr is not None:
|
|
109
|
+
for prepared_value in soap_body.values():
|
|
110
|
+
if prepared_value is expr:
|
|
111
|
+
return resource_param
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _property_value_key_for_deferred(matched_param: Param | None, param_source: str) -> str:
|
|
117
|
+
if matched_param is not None:
|
|
118
|
+
return matched_param.name
|
|
119
|
+
path_suffix = param_source.rsplit("/", 1)[-1]
|
|
120
|
+
return path_suffix.lower() if path_suffix else param_source
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _resolve_deferred_soap_request_body_exprs(env: SoapQueryBuilder) -> None:
|
|
124
|
+
deferred_names = get_deferred_prepare_names()
|
|
125
|
+
if not deferred_names:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for param_source, deferred_expr in list(env.soap_request_body.items()):
|
|
129
|
+
if not isinstance(deferred_expr, Expr):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if getattr(deferred_expr, "name", None) not in deferred_names:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
resolved = env.resolve(deferred_expr)
|
|
136
|
+
matched_param = _param_for_soap_body_key(env, param_source, expr=deferred_expr)
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
matched_param
|
|
140
|
+
and getattr(matched_param, "soap_body_value_type", None) == SOAP_BODY_VALUE_TYPE_CDATA
|
|
141
|
+
and resolved
|
|
142
|
+
):
|
|
143
|
+
env.soap_request_body[param_source] = MakeCDATA(resolved)
|
|
144
|
+
else:
|
|
145
|
+
env.soap_request_body[param_source] = resolved
|
|
146
|
+
|
|
147
|
+
property_value_key = _property_value_key_for_deferred(matched_param, param_source)
|
|
148
|
+
env.property_values[property_value_key] = resolved
|
|
149
|
+
|
|
150
|
+
if matched_param is None:
|
|
151
|
+
log.warning(
|
|
152
|
+
"SOAP deferred resolve: no Param matched for %r; property_values updated under %r",
|
|
153
|
+
param_source,
|
|
154
|
+
property_value_key,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
81
158
|
def _populate_soap_request_body_with_url_values(env: SoapQueryBuilder) -> None:
|
|
82
159
|
for prop in take(env.model.properties).values():
|
|
83
160
|
if not authorized(env.context, prop, Action.GETALL):
|
|
84
161
|
continue
|
|
162
|
+
|
|
85
163
|
env.call("soap_request_body", prop)
|
|
86
164
|
|
|
87
165
|
|
|
@@ -93,25 +171,24 @@ def soap_request_body(env: SoapQueryBuilder) -> None:
|
|
|
93
171
|
for param in env.params.values():
|
|
94
172
|
if not hasattr(param, "soap_body"):
|
|
95
173
|
continue
|
|
174
|
+
|
|
96
175
|
env.soap_request_body.update(param.soap_body)
|
|
97
176
|
|
|
98
177
|
_finalize_soap_request_body_resolve(env)
|
|
99
178
|
_populate_soap_request_body_with_url_values(env)
|
|
179
|
+
_resolve_deferred_soap_request_body_exprs(env)
|
|
100
180
|
|
|
101
181
|
|
|
102
182
|
@ufunc.resolver(SoapQueryBuilder, Property)
|
|
103
183
|
def soap_request_body(env: SoapQueryBuilder, prop: Property) -> None:
|
|
104
184
|
"""
|
|
105
|
-
Only care
|
|
185
|
+
Only care for properties that describe URL query parameters:
|
|
106
186
|
properties without `source` and with `prepare`.
|
|
107
|
-
We ignore the rest.
|
|
108
187
|
"""
|
|
109
188
|
if prop.external.name:
|
|
110
|
-
# Ignore properties with `source`
|
|
111
189
|
return None
|
|
112
190
|
|
|
113
191
|
if not isinstance(prop.external.prepare, Expr):
|
|
114
|
-
# Ignore properties without `prepare` expression
|
|
115
192
|
return None
|
|
116
193
|
|
|
117
194
|
resource_param = env(this=prop).resolve(prop.external.prepare)
|
|
@@ -129,25 +206,41 @@ def _get_final_soap_request_body_value(env: SoapQueryBuilder, property_name: str
|
|
|
129
206
|
|
|
130
207
|
@ufunc.resolver(SoapQueryBuilder, Property, Param)
|
|
131
208
|
def soap_request_body(env: SoapQueryBuilder, prop: Property, param: Param) -> None:
|
|
132
|
-
"""
|
|
133
|
-
|
|
209
|
+
"""Merge URL query params into the SOAP body; resolve non-deferred Expr; keep deferred Expr for a later pass."""
|
|
210
|
+
soap_body = getattr(param, "soap_body", None)
|
|
211
|
+
if not soap_body:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
param_source = next(iter(soap_body))
|
|
215
|
+
deferred_names = get_deferred_prepare_names()
|
|
216
|
+
|
|
134
217
|
final_value = _get_final_soap_request_body_value(env, prop.place, param_source)
|
|
135
218
|
|
|
219
|
+
if isinstance(final_value, Expr) and getattr(final_value, "name", None) not in deferred_names:
|
|
220
|
+
final_value = env.resolve(final_value)
|
|
221
|
+
|
|
222
|
+
is_deferred_expr = isinstance(final_value, Expr) and getattr(final_value, "name", None) in deferred_names
|
|
223
|
+
|
|
136
224
|
if final_value is NA:
|
|
137
|
-
# If value not in URL and DSA has no default - remove it from SOAP request completely
|
|
138
225
|
env.soap_request_body.pop(param_source, None)
|
|
139
226
|
final_value = None
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
227
|
+
|
|
228
|
+
elif is_deferred_expr:
|
|
229
|
+
env.soap_request_body[param_source] = final_value
|
|
230
|
+
|
|
231
|
+
else:
|
|
232
|
+
if param.soap_body_value_type == SOAP_BODY_VALUE_TYPE_CDATA and final_value:
|
|
233
|
+
soap_final_value = MakeCDATA(final_value)
|
|
234
|
+
else:
|
|
235
|
+
soap_final_value = final_value
|
|
236
|
+
|
|
143
237
|
env.soap_request_body[param_source] = soap_final_value
|
|
144
238
|
|
|
145
|
-
# Check if required property values exist
|
|
146
239
|
if prop.dtype.required and param_source not in env.soap_request_body:
|
|
147
240
|
raise MissingRequiredProperty(prop, prop=prop.name)
|
|
148
241
|
|
|
149
|
-
|
|
150
|
-
|
|
242
|
+
if not is_deferred_expr:
|
|
243
|
+
env.property_values.update({param.name: final_value})
|
|
151
244
|
|
|
152
245
|
|
|
153
246
|
@ufunc.resolver(SoapQueryBuilder, str)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from spinta import commands, exceptions
|
|
2
|
+
from spinta.components import Context, Model
|
|
3
|
+
from spinta.core.ufuncs import Expr
|
|
4
|
+
from spinta.datasets.backends.dataframe.components import DaskBackend
|
|
5
|
+
from spinta.datasets.backends.dataframe.ufuncs.query.ufuncs import COMPARE
|
|
6
|
+
from spinta.handlers import ErrorManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_compare_operators(expr: Expr) -> list[str]:
|
|
10
|
+
"""Recursively collect compare operator (filters) names from an Expr tree."""
|
|
11
|
+
if not isinstance(expr, Expr):
|
|
12
|
+
return []
|
|
13
|
+
filters: list[str] = []
|
|
14
|
+
if expr.name in COMPARE:
|
|
15
|
+
filters.append(expr.name)
|
|
16
|
+
for arg in expr.args:
|
|
17
|
+
filters.extend(_get_compare_operators(arg))
|
|
18
|
+
for val in expr.kwargs.values():
|
|
19
|
+
filters.extend(_get_compare_operators(val))
|
|
20
|
+
return filters
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@commands.check.register(Context, Model, DaskBackend)
|
|
24
|
+
def check(context: Context, model: Model, backend: DaskBackend) -> None:
|
|
25
|
+
# If there are any compare operators in the prepare expression,
|
|
26
|
+
# raise an error since they are not supported for Dask backend.
|
|
27
|
+
if (
|
|
28
|
+
model.external
|
|
29
|
+
and model.external.prepare
|
|
30
|
+
and (compare_operators := list(dict.fromkeys(_get_compare_operators(model.external.prepare))))
|
|
31
|
+
):
|
|
32
|
+
manager: ErrorManager = context.get("error_manager")
|
|
33
|
+
manager.handle_error(exceptions.DaskBackendCompareNotSupported(model, operators=compare_operators))
|
|
@@ -8,6 +8,7 @@ from spinta.components import Model, Property
|
|
|
8
8
|
from spinta.core.ufuncs import Env, Expr
|
|
9
9
|
|
|
10
10
|
from spinta.exceptions import UnknownMethod
|
|
11
|
+
from spinta.ufuncs.propertyresolver.components import PropertyResolver
|
|
11
12
|
from spinta.ufuncs.querybuilder.components import Selected
|
|
12
13
|
from spinta.utils.schema import NA
|
|
13
14
|
from spinta.datasets.backends.dataframe.components import DaskBackend
|
|
@@ -19,6 +20,7 @@ class DaskDataFrameQueryBuilder(Env):
|
|
|
19
20
|
dataframe: DataFrame
|
|
20
21
|
params: dict
|
|
21
22
|
url_query_params: Expr | None
|
|
23
|
+
property_resolver: PropertyResolver = None
|
|
22
24
|
|
|
23
25
|
def init(self, backend: DaskBackend, dataframe: DataFrame, params: dict) -> DaskDataFrameQueryBuilder:
|
|
24
26
|
return self(
|
|
@@ -55,6 +57,14 @@ class DaskDataFrameQueryBuilder(Env):
|
|
|
55
57
|
def default_resolver(self, expr, *args, **kwargs):
|
|
56
58
|
raise UnknownMethod(name=expr.name, expr=str(expr(*args, **kwargs)))
|
|
57
59
|
|
|
60
|
+
def resolve_property(self, *args, **kwargs) -> Property:
|
|
61
|
+
if self.property_resolver is None:
|
|
62
|
+
resolver = PropertyResolver(self.context)
|
|
63
|
+
resolver = resolver.init(model=self.model, ufunc_types=True)
|
|
64
|
+
self.property_resolver = resolver
|
|
65
|
+
result = self.property_resolver.resolve_property(*args, **kwargs)
|
|
66
|
+
return result
|
|
67
|
+
|
|
58
68
|
|
|
59
69
|
class DaskSelected(Selected):
|
|
60
70
|
# Item name in select list.
|