spinta 0.2.dev27__py3-none-any.whl → 0.2.dev29__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.
Files changed (54) hide show
  1. spinta/api/__init__.py +9 -2
  2. spinta/auth.py +1 -2
  3. spinta/backends/__init__.py +26 -1
  4. spinta/backends/components.py +3 -0
  5. spinta/backends/postgresql/helpers/exceptions.py +1 -3
  6. spinta/backends/postgresql/ufuncs/query/components.py +5 -7
  7. spinta/backends/postgresql/ufuncs/query/ufuncs.py +4 -4
  8. spinta/cli/helpers/admin/scripts/add_local_ids.py +1 -1
  9. spinta/cli/helpers/admin/scripts/remove_local_ids.py +1 -1
  10. spinta/cli/helpers/push/read.py +2 -2
  11. spinta/cli/helpers/store.py +1 -1
  12. spinta/cli/pii.py +1 -1
  13. spinta/components.py +6 -2
  14. spinta/datasets/backends/dataframe/backends/csv/components.py +3 -0
  15. spinta/datasets/backends/dataframe/backends/memory/components.py +3 -0
  16. spinta/datasets/backends/dataframe/commands/read.py +3 -15
  17. spinta/datasets/backends/dataframe/ufuncs/query/components.py +2 -3
  18. spinta/datasets/backends/dataframe/ufuncs/query/helpers.py +76 -0
  19. spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py +82 -23
  20. spinta/datasets/backends/helpers.py +52 -154
  21. spinta/datasets/backends/sql/commands/cast.py +2 -230
  22. spinta/datasets/backends/sql/commands/read.py +3 -5
  23. spinta/datasets/backends/sql/helpers.py +2 -7
  24. spinta/datasets/backends/sql/ufuncs/query/ufuncs.py +19 -11
  25. spinta/datasets/commands/cast.py +308 -0
  26. spinta/datasets/commands/load.py +1 -1
  27. spinta/datasets/components.py +3 -0
  28. spinta/datasets/enums.py +41 -0
  29. spinta/datasets/helpers.py +96 -3
  30. spinta/datasets/keymaps/components.py +4 -0
  31. spinta/datasets/keymaps/helpers.py +19 -0
  32. spinta/datasets/keymaps/redis.py +7 -0
  33. spinta/datasets/keymaps/sqlalchemy.py +13 -1
  34. spinta/formats/helpers.py +1 -1
  35. spinta/formats/html/commands.py +3 -2
  36. spinta/formats/html/helpers.py +1 -1
  37. spinta/manifests/tabular/helpers.py +44 -24
  38. spinta/middlewares.py +58 -0
  39. spinta/testing/client.py +2 -2
  40. spinta/testing/data.py +10 -10
  41. spinta/testing/utils.py +2 -2
  42. spinta/types/model.py +18 -5
  43. spinta/types/namespace.py +3 -1
  44. spinta/types/ref/link.py +1 -1
  45. spinta/utils/nestedstruct.py +2 -2
  46. spinta/utils/response.py +5 -2
  47. {spinta-0.2.dev27.dist-info → spinta-0.2.dev29.dist-info}/METADATA +2 -2
  48. {spinta-0.2.dev27.dist-info → spinta-0.2.dev29.dist-info}/RECORD +51 -51
  49. spinta/datasets/backends/sql/backends/oracle/types/__init__.py +0 -0
  50. spinta/datasets/backends/sql/backends/oracle/types/geometry/__init__.py +0 -0
  51. spinta/datasets/backends/sql/backends/oracle/types/geometry/encode.py +0 -41
  52. {spinta-0.2.dev27.dist-info → spinta-0.2.dev29.dist-info}/WHEEL +0 -0
  53. {spinta-0.2.dev27.dist-info → spinta-0.2.dev29.dist-info}/entry_points.txt +0 -0
  54. {spinta-0.2.dev27.dist-info → spinta-0.2.dev29.dist-info}/licenses/LICENSE +0 -0
spinta/api/__init__.py CHANGED
@@ -50,7 +50,7 @@ from spinta.exceptions import (
50
50
  error_response,
51
51
  )
52
52
  from spinta.formats.html.helpers import get_templates
53
- from spinta.middlewares import ContextMiddleware, StrictTransportSecurityMiddleware
53
+ from spinta.middlewares import ContextMiddleware, PathNormalizationMiddleware, StrictTransportSecurityMiddleware
54
54
  from spinta.urlparams import Version, get_response_type
55
55
 
56
56
  log = logging.getLogger(__name__)
@@ -370,7 +370,7 @@ async def error(request, exc):
370
370
  elif isinstance(exc, BaseError):
371
371
  status_code = exc.status_code
372
372
  errors = [error_response(exc)]
373
- headers = exc.headers
373
+ headers = dict(exc.headers)
374
374
  else:
375
375
  if isinstance(exc, HTTPException):
376
376
  status_code = exc.status_code
@@ -400,6 +400,11 @@ async def error(request, exc):
400
400
 
401
401
  response = {"errors": errors}
402
402
 
403
+ # Error responses can reflect request input (e.g. the requested path) and
404
+ # must never be stored by shared caches, otherwise they can be used for
405
+ # web cache poisoning.
406
+ headers["Cache-Control"] = "no-store"
407
+
403
408
  fmt = get_response_type(request.state.context, request)
404
409
  if fmt == "json" or fmt is None:
405
410
  return JSONResponse(
@@ -414,6 +419,7 @@ async def error(request, exc):
414
419
  }
415
420
 
416
421
  return templates.TemplateResponse(
422
+ request,
417
423
  "error.html",
418
424
  response,
419
425
  status_code=status_code,
@@ -467,6 +473,7 @@ def init(context: Context):
467
473
  StrictTransportSecurityMiddleware,
468
474
  value=config.http_strict_transport_security,
469
475
  ),
476
+ Middleware(PathNormalizationMiddleware),
470
477
  Middleware(ContextMiddleware, context=context),
471
478
  ]
472
479
 
spinta/auth.py CHANGED
@@ -538,7 +538,7 @@ class AdminToken(rfc6749.TokenMixin):
538
538
  return True
539
539
 
540
540
  def check_scope(self, scope: SCOPE_TYPE, **kwargs) -> bool:
541
- pass
541
+ return True
542
542
 
543
543
  def get_sub(self) -> str: # User.
544
544
  return "admin"
@@ -864,7 +864,6 @@ def authorized(
864
864
 
865
865
  # Unauthenticated clients can only access nodes if spinta config.access is open.
866
866
  unauthenticated = token.get_client_id() == get_default_auth_client_id(context)
867
-
868
867
  if unauthenticated and config.access < Access.open:
869
868
  if throw:
870
869
  raise AuthorizedClientsOnly()
@@ -590,7 +590,7 @@ def is_object_id(context: Context, value: str):
590
590
 
591
591
  @is_object_id.register(Context, Backend, Model, str)
592
592
  def is_object_id(context: Context, backend: Backend, model: Model, value: str):
593
- return is_object_id(context, backend, model.properties["_id"].dtype, value)
593
+ return is_object_id(context, backend, model.id_prop.dtype, value)
594
594
 
595
595
 
596
596
  @is_object_id.register(Context, Backend, PrimaryKey, str)
@@ -1940,6 +1940,31 @@ def cast_backend_to_python(context: Context, dtype: Ref, backend: Backend, data:
1940
1940
  return processed_data
1941
1941
 
1942
1942
 
1943
+ @commands.cast_backend_to_python.register(Context, ExternalRef, Backend, dict)
1944
+ def cast_backend_to_python(
1945
+ context: Context, dtype: ExternalRef, backend: Backend, data: Dict[str, Any], **kwargs
1946
+ ) -> Any:
1947
+ if not data:
1948
+ return data
1949
+
1950
+ processed_data = {}
1951
+ for key in data:
1952
+ prop = commands.resolve_property(dtype.prop.model, f"{dtype.prop.place}.{key}")
1953
+ if prop is not None:
1954
+ processed_data[key] = commands.cast_backend_to_python(context, prop, backend, data[key], **kwargs)
1955
+
1956
+ for prop in dtype.refprops:
1957
+ if prop.name not in processed_data and prop.name in data:
1958
+ processed_data[prop.name] = commands.cast_backend_to_python(
1959
+ context, prop, backend, data[prop.name], **kwargs
1960
+ )
1961
+
1962
+ if not processed_data or all(value is None for value in processed_data.values()):
1963
+ return None
1964
+
1965
+ return processed_data
1966
+
1967
+
1943
1968
  @commands.cast_backend_to_python.register(Context, Object, Backend, dict)
1944
1969
  def cast_backend_to_python(
1945
1970
  context: Context, dtype: Object, backend: Backend, data: Dict[str, Any], **kwargs
@@ -36,6 +36,9 @@ class Backend:
36
36
  result_builder_type: str = ""
37
37
  result_builder_class: Type[ResultBuilder]
38
38
 
39
+ # Some backends might only work with source given to models and some might not need it
40
+ model_requires_source: bool = False
41
+
39
42
  def __repr__(self):
40
43
  return f"<{self.__class__.__module__}.{self.__class__.__name__}(name={self.name!r}) at 0x{id(self):02x}>"
41
44
 
@@ -19,9 +19,7 @@ def create_exception(item: DataItem, error: psy_errors.ForeignKeyViolation):
19
19
  error_message = error.diag.message_detail
20
20
  if item.action is Action.DELETE:
21
21
  error_model = extractor.extract_error_model(error_message)
22
- return exceptions.ReferringObjectFound(
23
- item.model.properties.get("_id"), model=error_model, id=item.saved.get("_id")
24
- )
22
+ return exceptions.ReferringObjectFound(item.model.id_prop, model=error_model, id=item.saved.get("_id"))
25
23
  else:
26
24
  error_property_names = extractor.extract_error_property_names(error_message)
27
25
  error_ref_id = extractor.extract_error_ref_id(error_message)
@@ -57,11 +57,9 @@ class PgQueryBuilder(QueryBuilder):
57
57
  self.call("select", Expr("select"))
58
58
 
59
59
  if not self.aggregate:
60
- self.selected["_id"] = Selected(
61
- item=self.add_column(self.table.c["_id"]), prop=self.model.properties["_id"]
62
- )
60
+ self.selected["_id"] = Selected(item=self.add_column(self.table.c["_id"]), prop=self.model.id_prop)
63
61
  self.selected["_revision"] = Selected(
64
- item=self.add_column(self.table.c["_revision"]), prop=self.model.properties["_revision"]
62
+ item=self.add_column(self.table.c["_revision"]), prop=self.model.revision_prop
65
63
  )
66
64
  merged_selected = merge_with_page_selected_list(self.columns, self.page)
67
65
  merged_sorted = merge_with_page_sort(self.sort, self.page)
@@ -116,7 +114,7 @@ class PgQueryBuilder(QueryBuilder):
116
114
  else:
117
115
  rtable = self.backend.get_table(rmodel).alias()
118
116
 
119
- rpkey = self.backend.get_column(rtable, rmodel.properties["_id"])
117
+ rpkey = self.backend.get_column(rtable, rmodel.id_prop)
120
118
 
121
119
  if isinstance(lrkey, list) and not isinstance(rpkey, list):
122
120
  if len(lrkey) == 1:
@@ -199,10 +197,10 @@ class PgQueryBuilder(QueryBuilder):
199
197
  raise PropertyNotFound(model, property=prop)
200
198
 
201
199
  ltable = self.backend.get_table(inherit_model)
202
- lrkey = self.backend.get_column(ltable, inherit_model.properties["_id"])
200
+ lrkey = self.backend.get_column(ltable, inherit_model.id_prop)
203
201
 
204
202
  rtable = self.backend.get_table(base_model).alias()
205
- rpkey = self.backend.get_column(rtable, base_model.properties["_id"])
203
+ rpkey = self.backend.get_column(rtable, base_model.id_prop)
206
204
 
207
205
  if base_model.name in self.joins:
208
206
  if self.joins[base_model.name] == rtable:
@@ -287,7 +287,7 @@ def select(env, dtype: Ref):
287
287
  if not dtype.inherited:
288
288
  name = "_id"
289
289
  if env.query_params.prioritize_uri and uri is not None:
290
- fpr = ForeignProperty(None, dtype, dtype.model.properties["_id"].dtype)
290
+ fpr = ForeignProperty(None, dtype, dtype.model.id_prop.dtype)
291
291
  table = env.get_joined_table(fpr)
292
292
  column = table.c[uri.place]
293
293
  name = "_uri"
@@ -311,7 +311,7 @@ def select(env, dtype: ExternalRef):
311
311
  if dtype.model.given.pkeys or dtype.explicit:
312
312
  props = dtype.refprops
313
313
  else:
314
- props = [dtype.model.properties["_id"]]
314
+ props = [dtype.model.id_prop]
315
315
  for prop in props:
316
316
  name = f"{dtype.prop.place}.{prop.place}"
317
317
  column = table.c[name]
@@ -350,7 +350,7 @@ def _select_backref(env, dtype, is_array=False):
350
350
  return_columns = {}
351
351
 
352
352
  if commands.identifiable(refprop):
353
- id_ = fpr.right.prop.model.properties["_id"]
353
+ id_ = fpr.right.prop.model.id_prop
354
354
  column_name = id_.name
355
355
  label = f"{dtype.prop.name}.{column_name}"
356
356
  required_columns.append((column_name, label))
@@ -431,7 +431,7 @@ def _denorm_to_foreign_property(env: PgQueryBuilder, dtype: Denorm):
431
431
  and isinstance(root_ref_parent, Property)
432
432
  and isinstance(root_ref_parent.dtype, (Ref, BackRef))
433
433
  ):
434
- fpr = ForeignProperty(fpr, root_ref_parent.dtype, root_ref_parent.dtype.model.properties["_id"].dtype)
434
+ fpr = ForeignProperty(fpr, root_ref_parent.dtype, root_ref_parent.dtype.model.id_prop.dtype)
435
435
 
436
436
  if not root_ref_parent.dtype.inherited:
437
437
  break
@@ -25,7 +25,7 @@ def add_explicit_id_properties(context: Context, manifest: str, verbose: bool =
25
25
  if verbose:
26
26
  echo(f'Model "{model.model_type()}" has no `ref`, skipping.', err=True)
27
27
  continue
28
- prop = model.properties.get("_id")
28
+ prop = model.id_prop
29
29
  if prop is None or prop.explicitly_given:
30
30
  continue
31
31
 
@@ -19,7 +19,7 @@ def remove_explicit_id_properties(context: Context, manifest: str) -> int:
19
19
  for model in commands.get_models(context, manifest).values():
20
20
  if model.model_type().startswith(RESERVED_MODEL_PREFIX):
21
21
  continue
22
- prop = model.properties.get("_id")
22
+ prop = model.id_prop
23
23
  if prop is None or not prop.explicitly_given:
24
24
  continue
25
25
 
@@ -55,9 +55,9 @@ def _iter_model_rows(
55
55
  else:
56
56
  stream = read_model_data(context, model, limit, stop_on_error, params=params)
57
57
  for item in stream:
58
- if push_counter:
58
+ if push_counter is not None:
59
59
  push_counter.update(1)
60
- if model_push_counter:
60
+ if model_push_counter is not None:
61
61
  model_push_counter.update(1)
62
62
  yield PushRow(model, item)
63
63
 
@@ -166,4 +166,4 @@ def attach_backends(context: Context, store: Store, manifest: Manifest) -> None:
166
166
 
167
167
  def attach_keymaps(context: Context, store: Store) -> None:
168
168
  for keymap in store.keymaps.values():
169
- context.attach(f"keymap.{keymap.name}", lambda: keymap)
169
+ context.attach(f"keymap.{keymap.name}", lambda: keymap.copy())
spinta/cli/pii.py CHANGED
@@ -220,7 +220,7 @@ def detect(
220
220
  f"transaction.{resource.backend.name}", validate_and_return_begin, context, resource.backend
221
221
  )
222
222
  for keymap in store.keymaps.values():
223
- context.attach(f"keymap.{keymap.name}", lambda: keymap)
223
+ context.attach(f"keymap.{keymap.name}", lambda: keymap.copy())
224
224
 
225
225
  ns = commands.get_namespace(context, manifest, "")
226
226
  models = commands.traverse_ns_models(context, ns, manifest, Action.SEARCH)
spinta/components.py CHANGED
@@ -662,7 +662,6 @@ class Model(MetaData):
662
662
  comments: List[Comment] = None
663
663
  base: Base = None
664
664
  uri: str = None
665
- uri_prop: Property = None
666
665
  page: PageInfo = None
667
666
  features: str = None
668
667
  status: Status | None = None
@@ -675,6 +674,11 @@ class Model(MetaData):
675
674
  distribution_strategy: DistributionStrategy | None = None
676
675
  required_keymap_properties = None
677
676
 
677
+ # Quick access properties
678
+ uri_prop: Property | None = None
679
+ id_prop: Property
680
+ revision_prop: Property
681
+
678
682
  schema = {
679
683
  "keymap": {"type": "string"},
680
684
  "backend": {"type": "string"},
@@ -741,7 +745,7 @@ class Model(MetaData):
741
745
  # return self.name.split('/')[-1]
742
746
 
743
747
  def add_keymap_property_combination(self, given_props: List[Property]):
744
- extract_names = list([prop.name for prop in given_props])
748
+ extract_names = tuple([prop.place for prop in given_props])
745
749
  if extract_names not in self.required_keymap_properties:
746
750
  self.required_keymap_properties.append(extract_names)
747
751
 
@@ -6,6 +6,9 @@ from spinta.datasets.backends.dataframe.components import DaskBackend
6
6
  class Csv(DaskBackend):
7
7
  type: str = "dask/csv"
8
8
 
9
+ # Csv data is mainly one table, meaning model.source is usually not given
10
+ model_requires_source: bool = False
11
+
9
12
  @contextlib.contextmanager
10
13
  def begin(self):
11
14
  yield
@@ -6,6 +6,9 @@ from spinta.datasets.backends.dataframe.components import DaskBackend
6
6
  class MemoryDaskBackend(DaskBackend):
7
7
  type: str = "dask/memory"
8
8
 
9
+ # Memory backends can set model.source, but it can also leave it open and still work
10
+ model_requires_source: bool = False
11
+
9
12
  @contextlib.contextmanager
10
13
  def begin(self):
11
14
  yield
@@ -16,16 +16,14 @@ from spinta.core.ufuncs import Env, Expr
16
16
  from spinta.datasets.backends.dataframe.backends.memory.components import MemoryDaskBackend
17
17
  from spinta.datasets.backends.dataframe.components import DaskBackend
18
18
  from spinta.datasets.backends.dataframe.ufuncs.query.components import DaskDataFrameQueryBuilder
19
- from spinta.datasets.backends.helpers import handle_ref_key_assignment
20
19
  from spinta.datasets.components import Resource
21
20
  from spinta.datasets.helpers import encode_composite_string_id, get_enum_filters, get_ref_filters
22
- from spinta.datasets.keymaps.components import KeyMap
23
21
  from spinta.datasets.utils import iterparams
24
22
  from spinta.dimensions.enum.helpers import get_prop_enum
25
23
  from spinta.dimensions.param.components import ResolvedParams
26
24
  from spinta.exceptions import NoExternalName, PropertyNotFound, ValueNotInEnum
27
25
  from spinta.manifests.components import Manifest
28
- from spinta.types.datatype import Base32, Boolean, DataType, DateTime, Integer, Number, PrimaryKey, Ref
26
+ from spinta.types.datatype import Base32, Boolean, DataType, DateTime, Integer, Number, Ref
29
27
  from spinta.typing import ObjectData
30
28
  from spinta.ufuncs.helpers import merge_formulas
31
29
  from spinta.ufuncs.querybuilder.components import Selected
@@ -182,7 +180,7 @@ def _select_primary_key(model: Model, df: DataFrame):
182
180
  result = _select(prop, df)
183
181
  else:
184
182
  result = [_select(prop, df) for prop in pkeys]
185
- return Selected(prop=model.properties["_id"], prep=result)
183
+ return Selected(prop=model.id_prop, prep=result)
186
184
 
187
185
 
188
186
  def get_pkeys_if_ref(prop: Property) -> list[str]:
@@ -258,7 +256,6 @@ def dask_get_all(
258
256
  params: dict | None = None,
259
257
  ):
260
258
  params = params or {}
261
- keymap: KeyMap = context.get(f"keymap.{model.keymap.name}")
262
259
 
263
260
  query = merge_formulas(model.external.prepare, query)
264
261
  query = merge_formulas(query, get_enum_filters(context, model))
@@ -279,16 +276,7 @@ def dask_get_all(
279
276
  for key, sel in env_selected.items():
280
277
  val = _get_row_value(context, row, sel, env.params)
281
278
  if sel.prop:
282
- if isinstance(sel.prop.dtype, PrimaryKey):
283
- if isinstance(val, list):
284
- val = [
285
- list_value.get("_id", list_value) if isinstance(list_value, dict) else list_value
286
- for list_value in val
287
- ]
288
- val = keymap.encode(sel.prop.model.model_type(), val)
289
- elif isinstance(sel.prop.dtype, Ref):
290
- val = handle_ref_key_assignment(context, keymap, env, val, sel.prop.dtype)
291
- elif is_custom_id_prop(sel.prop) and isinstance(val, list) and not isinstance(sel.prop.dtype, Base32):
279
+ if is_custom_id_prop(sel.prop) and isinstance(val, list) and not isinstance(sel.prop.dtype, Base32):
292
280
  val = encode_composite_string_id(val, model.external.pkeys)
293
281
  res[key] = val
294
282
  res = flat_dicts_to_nested(res, list_keys=list_keys)
@@ -15,6 +15,8 @@ from spinta.ufuncs.propertyresolver.components import PropertyResolver
15
15
  from spinta.ufuncs.querybuilder.components import Func, Selected
16
16
  from spinta.utils.schema import NA
17
17
 
18
+ RESERVED_COUNT_PROP = "__dask_count"
19
+
18
20
 
19
21
  class DaskDataFrameQueryBuilder(Env):
20
22
  backend: DaskBackend
@@ -90,9 +92,6 @@ class DaskSelected(Selected):
90
92
  prep: Any = NA
91
93
 
92
94
 
93
- RESERVED_COUNT_PROP = "__dask_count"
94
-
95
-
96
95
  @dataclasses.dataclass
97
96
  class Count(Func):
98
97
  pass
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from spinta.datasets.backends.dataframe.ufuncs.query.components import (
4
+ DaskDataFrameQueryBuilder,
5
+ )
6
+ from spinta.datasets.backends.dataframe.ufuncs.query.components import (
7
+ DaskSelected as Selected,
8
+ )
9
+ from spinta.exceptions import (
10
+ GivenValueCountMissmatch,
11
+ PropertyNotPartOfRefProps,
12
+ )
13
+ from spinta.types.datatype import ExternalRef, Ref
14
+
15
+
16
+ def select_ref_foreign_key_properties(
17
+ env: DaskDataFrameQueryBuilder, dtype: Ref, *, properties: list = None
18
+ ) -> dict[str, Selected]:
19
+ if properties is None:
20
+ if dtype.prop.external and dtype.prop.external.prepare:
21
+ properties = env(this=dtype.prop).resolve(dtype.prop.external.prepare)
22
+
23
+ prep = {}
24
+ given_mapping_count = len(properties) if properties else 1
25
+ if len(dtype.refprops) != given_mapping_count:
26
+ raise GivenValueCountMissmatch(dtype, given_count=given_mapping_count, expected_count=len(dtype.refprops))
27
+
28
+ if properties is None:
29
+ refprop = dtype.refprops[0]
30
+ # Currently, dask can only access its own dataframe columns, so we we have to use dtype.prop.external.name as source.
31
+ prep[refprop.name] = Selected(item=dtype.prop.external.name, prop=refprop)
32
+ else:
33
+ for i, prop in enumerate(dtype.refprops):
34
+ sel = env.call("select", properties[i])
35
+ if sel.prop is None:
36
+ sel.prop = prop
37
+ prep[prop.name] = sel
38
+ return prep
39
+
40
+
41
+ def select_external_ref_foreign_key_properties(
42
+ env: DaskDataFrameQueryBuilder, dtype: ExternalRef, *, properties: list = None, target: str = None
43
+ ) -> dict[str, Selected]:
44
+ if properties is None:
45
+ if dtype.prop.external and dtype.prop.external.prepare:
46
+ properties = env(this=dtype.prop).resolve(dtype.prop.external.prepare)
47
+
48
+ if target is not None:
49
+ value = next((prop for prop in dtype.refprops if prop.name == target), None)
50
+ if value is None:
51
+ raise PropertyNotPartOfRefProps(dtype, prop=target, refprops=list(prop_.name for prop_ in dtype.refprops))
52
+ target = value
53
+
54
+ prep = {}
55
+ given_mapping_count = len(properties) if properties else 1
56
+ if len(dtype.refprops) != given_mapping_count:
57
+ raise GivenValueCountMissmatch(dtype, given_count=given_mapping_count, expected_count=len(dtype.refprops))
58
+
59
+ if properties is None:
60
+ refprop = dtype.refprops[0]
61
+ # Currently, dask can only access its own dataframe columns, so we we have to use dtype.prop.external.name as source.
62
+ prep[refprop.name] = Selected(item=dtype.prop.external.name, prop=refprop)
63
+ else:
64
+ if target is not None:
65
+ pos = dtype.refprops.index(target)
66
+ sel = env.call("select", properties[pos])
67
+ if sel.prop is None:
68
+ sel.prop = target
69
+ prep[target.name] = sel
70
+ else:
71
+ for i, prop in enumerate(dtype.refprops):
72
+ sel = env.call("select", properties[i])
73
+ if sel.prop is None:
74
+ sel.prop = prop
75
+ prep[prop.name] = sel
76
+ return prep
@@ -5,7 +5,6 @@ from typing import Any, Dict, Iterator, List, Tuple
5
5
 
6
6
  from dask.dataframe import Series
7
7
 
8
- from spinta.auth import authorized
9
8
  from spinta.backends.helpers import is_custom_id_prop, is_custom_revision_prop
10
9
  from spinta.components import Property
11
10
  from spinta.core.enums import Action
@@ -18,7 +17,13 @@ from spinta.datasets.backends.dataframe.ufuncs.query.components import (
18
17
  from spinta.datasets.backends.dataframe.ufuncs.query.components import (
19
18
  DaskSelected as Selected,
20
19
  )
20
+ from spinta.datasets.backends.dataframe.ufuncs.query.helpers import (
21
+ select_external_ref_foreign_key_properties,
22
+ select_ref_foreign_key_properties,
23
+ )
21
24
  from spinta.datasets.components import Param
25
+ from spinta.datasets.enums import ExternalIdPattern
26
+ from spinta.datasets.helpers import authorized_or_system_request
22
27
  from spinta.datasets.utils import iterparams
23
28
  from spinta.exceptions import (
24
29
  InvalidArgumentInExpression,
@@ -26,13 +31,23 @@ from spinta.exceptions import (
26
31
  SourceCannotBeList,
27
32
  SourceOrPrepareNotAllowed,
28
33
  )
29
- from spinta.types.datatype import Boolean, DataType, Integer, Number, PrimaryKey, Ref
34
+ from spinta.types.datatype import Boolean, DataType, ExternalRef, Integer, Number, PrimaryKey, Ref
30
35
  from spinta.types.text.components import Text
31
36
  from spinta.ufuncs.components import ForeignProperty
37
+ from spinta.ufuncs.querybuilder.helpers import process_literal_value
32
38
  from spinta.utils.data import take
33
39
  from spinta.utils.schema import NA
34
40
 
35
41
 
42
+ @ufunc.resolver(DaskDataFrameQueryBuilder, Expr)
43
+ def testlist(env: DaskDataFrameQueryBuilder, expr: Expr) -> tuple:
44
+ args, kwargs = expr.resolve(env)
45
+ result = []
46
+ for arg in args:
47
+ result.append(process_literal_value(arg))
48
+ return tuple(result)
49
+
50
+
36
51
  @ufunc.resolver(DaskDataFrameQueryBuilder, Expr, name="and")
37
52
  def and_(env, expr):
38
53
  args, kwargs = expr.resolve(env)
@@ -152,7 +167,7 @@ def select(env: DaskDataFrameQueryBuilder, expr: Expr):
152
167
  for prop in take(["_id", "_revision", all], env.model.properties).values():
153
168
  if prop.name == "_revision" and not is_custom_revision_prop(prop):
154
169
  continue
155
- if authorized(env.context, prop, Action.GETALL):
170
+ if authorized_or_system_request(env.context, prop, Action.GETALL):
156
171
  env.selected[prop.place] = env.call("select", prop)
157
172
 
158
173
 
@@ -182,7 +197,7 @@ def _get_property_for_select(
182
197
  # then how prepare context should be defined? Probably resolvers
183
198
  # should be called with a different env class?
184
199
  # tag:resolving_private_properties_in_prepare_context
185
- nested or authorized(env.context, prop, Action.SEARCH)
200
+ nested or authorized_or_system_request(env.context, prop, Action.SEARCH)
186
201
  ):
187
202
  return prop
188
203
  else:
@@ -199,7 +214,7 @@ def select(env: DaskDataFrameQueryBuilder, prop: Property) -> Selected:
199
214
  if prop.place not in env.resolved:
200
215
  if isinstance(prop.external, list):
201
216
  raise SourceCannotBeList(prop)
202
- if prop.external.name and prop.external.prepare is not NA:
217
+ if prop.external and prop.external.prepare is not NA:
203
218
  # If property doesn't have external name - it describes query parameter
204
219
  # If `prepare` formula is given, evaluate formula.
205
220
  if isinstance(prop.external.prepare, Expr):
@@ -213,13 +228,6 @@ def select(env: DaskDataFrameQueryBuilder, prop: Property) -> Selected:
213
228
  # properties.
214
229
  # tag:resolving_private_properties_in_prepare_context
215
230
  result = env.call("select", prop.dtype, result)
216
- elif prop.external.prepare is not NA:
217
- # property without external name may be evaluated already, if it is use the value
218
- if isinstance(prop.external.prepare, Expr):
219
- result = env(this=prop).resolve(prop.external.prepare)
220
- result = env.call("select", prop.dtype, result)
221
- else:
222
- result = Selected(prop=prop, prep=prop.external.prepare)
223
231
  elif prop.external and prop.external.name:
224
232
  # If prepare is not given, then take value from `source`.
225
233
  result = env.call("select", prop.dtype)
@@ -306,18 +314,22 @@ def select(
306
314
  dtype: PrimaryKey,
307
315
  ) -> Selected:
308
316
  model = dtype.prop.model
309
- pkeys = model.external.pkeys
310
-
317
+ pkeys = (model.base and model.base.pk) or model.external.pkeys
311
318
  if not pkeys:
312
319
  # If primary key is not specified use all properties to uniquely
313
320
  # identify row.
314
321
  pkeys = take(model.properties).values()
315
322
 
316
- if len(pkeys) == 1:
317
- prop = pkeys[0]
318
- result = env.call("select", prop)
319
- else:
320
- result = [env.call("select", prop) for prop in pkeys]
323
+ result = {
324
+ ExternalIdPattern.ID_KEY.value: {prop.name: env.call("select", prop) for prop in pkeys},
325
+ ExternalIdPattern.COMBINATIONS_KEY.value: {},
326
+ }
327
+ for required_pk_combination in model.required_keymap_properties:
328
+ combination_result = {}
329
+ for prop_name in required_pk_combination:
330
+ prop = env.call("_resolve_property", prop_name)
331
+ combination_result[prop.name] = env.call("select", prop)
332
+ result[ExternalIdPattern.COMBINATIONS_KEY.value][required_pk_combination] = combination_result
321
333
  return Selected(prop=dtype.prop, prep=result)
322
334
 
323
335
 
@@ -331,7 +343,10 @@ def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: GetAttr) -> Selecte
331
343
  resolved_prep = env.call("select", prep)
332
344
 
333
345
  result = {}
334
- result["_id"] = Selected(prop=dtype.prop, prep=resolved_prep)
346
+ if not dtype.inherited:
347
+ refprop = dtype.refprops[0]
348
+ result[ExternalIdPattern.ID_KEY.value] = Selected(prop=dtype.prop, prep={refprop.name: resolved_prep})
349
+
335
350
  for prop in dtype.properties.values():
336
351
  sel = env.call("select", prop)
337
352
  result[prop.name] = sel
@@ -341,7 +356,7 @@ def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: GetAttr) -> Selecte
341
356
 
342
357
  @ufunc.resolver(DaskDataFrameQueryBuilder, Ref, object)
343
358
  def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: Any) -> Selected:
344
- fpr = ForeignProperty(None, dtype, dtype.model.properties["_id"].dtype)
359
+ fpr = ForeignProperty(None, dtype, dtype.model.id_prop.dtype)
345
360
  return Selected(
346
361
  prop=dtype.prop,
347
362
  prep=env.call("select", fpr, fpr.right.prop),
@@ -351,7 +366,51 @@ def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: Any) -> Selected:
351
366
  @ufunc.resolver(DaskDataFrameQueryBuilder, Ref)
352
367
  def select(env: DaskDataFrameQueryBuilder, dtype: Ref) -> Selected:
353
368
  prep = {}
354
- prep["_id"] = Selected(item=dtype.prop.external.name, prop=dtype.prop)
369
+ if not dtype.inherited:
370
+ prep[ExternalIdPattern.ID_KEY.value] = Selected(
371
+ prop=dtype.prop, prep=select_ref_foreign_key_properties(env, dtype)
372
+ )
373
+
374
+ for prop in dtype.properties.values():
375
+ sel = env.call("select", prop)
376
+ prep[prop.name] = sel
377
+
378
+ return Selected(prop=dtype.prop, prep=prep)
379
+
380
+
381
+ @ufunc.resolver(DaskDataFrameQueryBuilder, Ref, (list, tuple))
382
+ def select(env: DaskDataFrameQueryBuilder, dtype: Ref, data: list | tuple) -> Selected:
383
+ prep = {}
384
+ if not dtype.inherited:
385
+ prep[ExternalIdPattern.ID_KEY.value] = Selected(
386
+ prop=dtype.prop, prep=select_ref_foreign_key_properties(env, dtype, properties=data)
387
+ )
388
+
389
+ for prop in dtype.properties.values():
390
+ sel = env.call("select", prop)
391
+ prep[prop.name] = sel
392
+
393
+ return Selected(prop=dtype.prop, prep=prep)
394
+
395
+
396
+ @ufunc.resolver(DaskDataFrameQueryBuilder, ExternalRef)
397
+ def select(env: DaskDataFrameQueryBuilder, dtype: ExternalRef) -> Selected:
398
+ prep = {}
399
+ if not dtype.inherited:
400
+ prep.update(select_external_ref_foreign_key_properties(env, dtype))
401
+
402
+ for prop in dtype.properties.values():
403
+ sel = env.call("select", prop)
404
+ prep[prop.name] = sel
405
+
406
+ return Selected(prop=dtype.prop, prep=prep)
407
+
408
+
409
+ @ufunc.resolver(DaskDataFrameQueryBuilder, ExternalRef, (list, tuple))
410
+ def select(env: DaskDataFrameQueryBuilder, dtype: ExternalRef, data: list | tuple) -> Selected:
411
+ prep = {}
412
+ if not dtype.inherited:
413
+ prep.update(select_external_ref_foreign_key_properties(env, dtype, properties=data))
355
414
 
356
415
  for prop in dtype.properties.values():
357
416
  sel = env.call("select", prop)
@@ -434,7 +493,7 @@ def select(
434
493
  def select(env: DaskDataFrameQueryBuilder, fpr: ForeignProperty, item: Bind):
435
494
  model = fpr.right.prop.model
436
495
  prop = env.call("_resolve_property", item)
437
- if authorized(env.context, prop, Action.SEARCH):
496
+ if authorized_or_system_request(env.context, prop, Action.SEARCH):
438
497
  return env.call("select", fpr, prop)
439
498
  else:
440
499
  raise PropertyNotFound(model, property=item.name)