spinta 0.2.dev21__py3-none-any.whl → 0.2.dev23__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/auth.py CHANGED
@@ -58,6 +58,7 @@ from spinta.exceptions import (
58
58
  InvalidClientsKeymapStructure,
59
59
  InvalidScopes,
60
60
  InvalidClientFileFormat,
61
+ ModelNotFound,
61
62
  )
62
63
  from spinta.utils import passwords
63
64
  from spinta.utils.config import get_clients_path, get_keymap_path, get_id_path, get_helpers_path
@@ -858,40 +859,46 @@ def authorized(
858
859
  scope_formatter: ScopeFormatterFunc = None,
859
860
  ):
860
861
  config: Config = context.get("config")
862
+
863
+ # Disable access to nodes that have lower access level than config.access
864
+ if config.access > node.access:
865
+ if throw:
866
+ raise ModelNotFound(model=node.name)
867
+ return False
868
+
861
869
  token = context.get("auth.token")
862
- # Unauthorized clients can only access open nodes.
863
- unauthorized = token.get_client_id() == get_default_auth_client_id(context)
864
- open_node = node.access >= Access.open
865
- if unauthorized and not open_node:
870
+
871
+ # Unauthenticated clients can only access nodes if spinta config.access is open.
872
+ unauthenticated = token.get_client_id() == get_default_auth_client_id(context)
873
+
874
+ if unauthenticated and config.access < Access.open:
866
875
  if throw:
867
876
  raise AuthorizedClientsOnly()
868
877
  else:
869
878
  return False
870
879
 
871
- # Private nodes can only be accessed with explicit node scope.
880
+ # Add explicit node scope
872
881
  scopes = [node]
873
882
 
874
- # Protected and higher level nodes can be accessed with parent nodes scopes.
875
- if node.access > Access.private:
876
- ns = None
877
-
878
- if isinstance(node, Property):
879
- # Hidden nodes also require explicit scope.
880
- # XXX: `hidden` parameter should only be used for API control, not
881
- # access control. See docs.
882
- if not node.hidden:
883
- scopes.append(node.model)
884
- scopes.append(node.model.ns)
885
- ns = node.model.ns
886
- elif isinstance(node, Model):
887
- scopes.append(node.ns)
888
- ns = node.ns
889
- elif isinstance(node, Namespace):
890
- ns = node
891
-
892
- # Add all parent namespace scopes too.
893
- if ns:
894
- scopes.extend(ns.parents())
883
+ # Add parent node scopes
884
+ ns = None
885
+ if isinstance(node, Property):
886
+ # Hidden nodes also require explicit scope.
887
+ # XXX: `hidden` parameter should only be used for API control, not
888
+ # access control. See docs.
889
+ if not node.hidden:
890
+ scopes.append(node.model)
891
+ scopes.append(node.model.ns)
892
+ ns = node.model.ns
893
+ elif isinstance(node, Model):
894
+ scopes.append(node.ns)
895
+ ns = node.ns
896
+ elif isinstance(node, Namespace):
897
+ ns = node
898
+
899
+ # Add all parent namespace scopes too.
900
+ if ns:
901
+ scopes.extend(ns.parents())
895
902
 
896
903
  # Build scope names.
897
904
  scope_formatter = scope_formatter or config.scope_formatter
spinta/cli/admin.py CHANGED
@@ -2,15 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import pathlib
5
- import sys
6
5
  from typing import Optional, List
7
6
 
8
7
  from typer import Context as TyperContext, Argument
9
8
  from typer import Option
10
- from typer import echo
11
9
 
12
10
  from spinta.cli.helpers.admin.components import ADMIN_SCRIPT_TYPE
13
11
  from spinta.cli.helpers.admin.registry import admin_script_registry
12
+ from spinta.cli.helpers.message import cli_error
14
13
  from spinta.cli.helpers.script.components import ScriptStatusCache
15
14
  from spinta.cli.helpers.script.core import run_specific_script
16
15
  from spinta.cli.helpers.script.helpers import sort_scripts_by_required
@@ -48,26 +47,30 @@ def admin(
48
47
  False,
49
48
  "-c",
50
49
  "--check",
51
- help=("Only runs script checks, skipping execution part (used to find out what scripts are needed to run)."),
50
+ help="Only runs script checks, skipping execution part (used to find out what scripts are needed to run).",
52
51
  ),
53
52
  input_path: Optional[pathlib.Path] = Option(
54
53
  None,
55
54
  "-i",
56
55
  "--input",
57
- help=("Path to input file (some scripts might require extra data). If not given, script will read from stdin."),
56
+ help="Path to input file (some scripts might require extra data). If not given, script will read from stdin.",
57
+ ),
58
+ output_path: Optional[pathlib.Path] = Option(
59
+ None,
60
+ "-o",
61
+ "--output",
62
+ help="Path to output file (some scripts might write extra data). If not given, script will write to stdout.",
58
63
  ),
59
64
  ):
60
65
  context = configure_context(ctx.obj)
61
66
 
62
67
  if force and check_only:
63
- echo("Cannot run force mode with check only mode", err=True)
64
- sys.exit(1)
68
+ cli_error("Cannot run force mode with check only mode")
65
69
 
66
70
  load_config(context, ensure_config_dir=ensure_config_dir)
67
71
 
68
72
  if not scripts:
69
- echo("At least one script needs to be specified", err=True)
70
- sys.exit(1)
73
+ cli_error("At least one script needs to be specified")
71
74
 
72
75
  script_objects = {}
73
76
  for script in scripts:
@@ -86,5 +89,6 @@ def admin(
86
89
  script_name=script,
87
90
  check_only=check_only,
88
91
  input_path=input_path,
92
+ output_path=output_path,
89
93
  status_cache=status_cache,
90
94
  )
@@ -15,3 +15,4 @@ class AdminScript(ScriptBase):
15
15
  class Script(enum.Enum):
16
16
  DEDUPLICATE = "deduplicate"
17
17
  CHANGELOG = "changelog"
18
+ ENUM_LIST = "enum_list"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from spinta.cli.helpers.admin.components import AdminScript, Script, ADMIN_SCRIPT_TYPE
4
4
  from spinta.cli.helpers.admin.scripts.changelog import migrate_changelog_duplicates, cli_requires_changelog_migrations
5
5
  from spinta.cli.helpers.admin.scripts.deduplicate import migrate_duplicates, cli_requires_deduplicate_migrations
6
+ from spinta.cli.helpers.admin.scripts.enums import gather_invalid_enum_values
6
7
  from spinta.cli.helpers.script.components import ScriptTarget, ScriptTag
7
8
  from spinta.cli.helpers.script.registry import script_registry
8
9
  from spinta.cli.helpers.upgrade.components import Script as UpgradeScript, UPGRADE_SCRIPT_TYPE
@@ -29,3 +30,6 @@ script_registry.register(
29
30
  targets={ScriptTarget.BACKEND.value},
30
31
  )
31
32
  )
33
+ script_registry.register(
34
+ AdminScript(name=Script.ENUM_LIST.value, run=gather_invalid_enum_values, targets={ScriptTarget.BACKEND.value})
35
+ )
@@ -0,0 +1,148 @@
1
+ import csv
2
+ import pathlib
3
+ import sys
4
+ from contextlib import nullcontext
5
+ from typing import Iterator
6
+
7
+ from multipledispatch import dispatch
8
+ from dataclasses import dataclass, field
9
+
10
+ from spinta import commands
11
+ from spinta.backends import Backend
12
+ from spinta.backends.postgresql.components import PostgreSQL
13
+ from spinta.cli.helpers.script.helpers import ensure_store_is_loaded
14
+ from spinta.components import Context, Model, Property
15
+ from spinta.core.ufuncs import Expr
16
+ from spinta.manifests.components import Manifest
17
+
18
+ import sqlalchemy as sa
19
+
20
+ from spinta.ufuncs.resultbuilder.components import EnumResultBuilder
21
+
22
+
23
+ @dispatch(Backend, Property)
24
+ def gather_unique_property_values() -> list:
25
+ raise NotImplementedError
26
+
27
+
28
+ @dispatch(PostgreSQL, Property)
29
+ def gather_unique_property_values(backend: PostgreSQL, prop: Property) -> list:
30
+ table = backend.get_table(prop)
31
+ column = backend.get_column(table, prop)
32
+ result = backend.engine.execute(sa.select(column).distinct()).scalars().all()
33
+ return result
34
+
35
+
36
+ @dispatch(Property)
37
+ def gather_unique_property_values(prop: Property) -> list:
38
+ return gather_unique_property_values(prop.dtype.backend, prop)
39
+
40
+
41
+ def get_models_with_enums(context: Context, manifest: Manifest) -> Iterator[Model]:
42
+ for model in commands.get_models(context, manifest).values():
43
+ for prop in model.flatprops.values():
44
+ if prop.enum:
45
+ yield model
46
+ break
47
+
48
+
49
+ @dataclass
50
+ class InvalidEnumProperty:
51
+ prop: Property
52
+ invalid_values: list = field(default_factory=list)
53
+
54
+ def add_invalid_value(self, value: object):
55
+ if value not in self.invalid_values:
56
+ self.invalid_values.append(value)
57
+
58
+
59
+ @dataclass
60
+ class InvalidEnumModel:
61
+ model: Model
62
+ enum_props: dict[str, InvalidEnumProperty] = field(default_factory=dict)
63
+
64
+ def add_invalid_value(self, prop: Property, value: object):
65
+ self.get_prop(prop).add_invalid_value(value)
66
+
67
+ def get_prop(self, prop: Property) -> InvalidEnumProperty:
68
+ if prop.place not in self.enum_props:
69
+ self.enum_props[prop.place] = InvalidEnumProperty(prop=prop)
70
+ return self.enum_props[prop.place]
71
+
72
+
73
+ def gather_invalid_enum_values(context: Context, output_path: pathlib.Path | None = None, **kwargs):
74
+ store = ensure_store_is_loaded(context)
75
+ manifest = store.manifest
76
+ invalid_models = {}
77
+ with context:
78
+ for model in get_models_with_enums(context, manifest):
79
+ enum_model = InvalidEnumModel(model=model)
80
+ for prop in model.flatprops.values():
81
+ if not prop.enum:
82
+ continue
83
+
84
+ enum_contains_expr = any(isinstance(enum.prepare, Expr) for enum in prop.enum.values())
85
+ values = gather_unique_property_values(prop)
86
+ for value in values:
87
+ if value is None and not prop.dtype.required:
88
+ continue
89
+ elif value is None:
90
+ enum_model.add_invalid_value(prop, value)
91
+ continue
92
+
93
+ if str(value) in prop.enum:
94
+ continue
95
+
96
+ check_value = value
97
+ if enum_contains_expr:
98
+ for enum in prop.enum.values():
99
+ env = EnumResultBuilder(context).init(value)
100
+ val = env.resolve(enum.prepare)
101
+ if env.has_value_changed:
102
+ check_value = val
103
+ break
104
+
105
+ if str(check_value) not in prop.enum:
106
+ enum_model.add_invalid_value(prop, value)
107
+
108
+ if enum_model.enum_props:
109
+ invalid_models[model.model_type()] = enum_model
110
+
111
+ output_invalid_enums_to_csv(invalid_models, output_path)
112
+
113
+
114
+ def output_invalid_enums_to_csv(invalid_models: dict, output_path: pathlib.Path | None = None):
115
+ stream_ctx = open(output_path, "w") if output_path else nullcontext(sys.stdout)
116
+ with stream_ctx as stream:
117
+ writer = csv.DictWriter(stream, fieldnames=["model", "property", "invalid_value"], lineterminator="\n")
118
+ writer.writeheader()
119
+ for model_key, model in sorted(invalid_models.items()):
120
+ model_written = False
121
+
122
+ for prop_name, prop in model.enum_props.items():
123
+ it = iter(prop.invalid_values)
124
+ first = next(it, None)
125
+ if first is None:
126
+ continue
127
+
128
+ model_name = ""
129
+ if not model_written:
130
+ model_name = model_key
131
+ model_written = True
132
+
133
+ writer.writerow(
134
+ {
135
+ "model": model_name,
136
+ "property": prop_name,
137
+ "invalid_value": first,
138
+ }
139
+ )
140
+
141
+ for value in it:
142
+ writer.writerow(
143
+ {
144
+ "model": "",
145
+ "property": "",
146
+ "invalid_value": value,
147
+ }
148
+ )
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  import tqdm
2
4
  from click import echo
3
5
  from click.exceptions import Exit
@@ -9,7 +11,9 @@ def cli_error(message: str):
9
11
 
10
12
 
11
13
  def cli_message(message: str, progress_bar: tqdm.tqdm = None):
14
+ # https://pubs.opengroup.org/onlinepubs/9799919799/
15
+ # This documentation states that `stderr` should be used for diagnostic messages (in our case status)
12
16
  if progress_bar is not None:
13
- progress_bar.write(message)
17
+ progress_bar.write(message, file=sys.stderr)
14
18
  else:
15
- echo(message)
19
+ echo(message, err=True)
@@ -31,7 +31,7 @@ class _ScriptMeta(type):
31
31
 
32
32
  class ScriptBase(metaclass=_ScriptMeta):
33
33
  """
34
- Represents an script with optional preconditions and target constraints.
34
+ Represents a script with optional preconditions and target constraints.
35
35
 
36
36
  Targets and tags are mainly used to be able to filter specific scripts. Main use case would be to set target as
37
37
  specific object, like sqlalchemy keymap and tag as migration, so then you could filter all database migration
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from click import echo
4
-
3
+ from spinta.cli.helpers.message import cli_message
5
4
  from spinta.cli.helpers.script.components import ScriptStatus, ScriptBase, ScriptStatusCache
6
5
  from spinta.cli.helpers.script.helpers import sort_scripts_by_required, script_check_status_message
7
6
  from spinta.cli.helpers.script.registry import script_registry
@@ -54,7 +53,7 @@ def run_specific_script(
54
53
  if force:
55
54
  status = ScriptStatus.FORCED
56
55
 
57
- echo(script_check_status_message(script_name, status))
56
+ cli_message(script_check_status_message(script_name, status))
58
57
  if status in (ScriptStatus.FORCED, ScriptStatus.REQUIRED) and not check_only:
59
58
  script.run(context, destructive=destructive, **kwargs)
60
59
  if status_cache is not None:
@@ -73,7 +72,7 @@ def check_script(
73
72
  script = script.value
74
73
 
75
74
  if not script_registry.contains(script_type, script):
76
- echo(f"Warning: {script_type!r} script {script!r} was not found", err=True)
75
+ cli_message(f"Warning: {script_type!r} script {script!r} was not found")
77
76
  return ScriptStatus.SKIPPED
78
77
 
79
78
  script = script_registry.get(script_type, script)
@@ -92,9 +91,8 @@ def check_script(
92
91
  ScriptStatus.REQUIRED,
93
92
  ScriptStatus.SKIPPED,
94
93
  ):
95
- echo(
96
- f"Warning: {required_script_type!r} script {required_script!r} requirement is not met for {script.name!r} script",
97
- err=True,
94
+ cli_message(
95
+ f"Warning: {script_type!r} script {required_script!r} requirement is not met for {script.name!r} script",
98
96
  )
99
97
  return ScriptStatus.SKIPPED
100
98
 
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict, deque
4
4
 
5
- from typer import echo
6
5
 
6
+ from spinta.cli.helpers.message import cli_message
7
7
  from spinta.cli.helpers.script.components import ScriptStatus
8
8
  from spinta.cli.helpers.upgrade.components import UpgradeScript
9
9
  from spinta.components import Context, Store
@@ -44,7 +44,7 @@ def sort_scripts_by_required(scripts: dict[str, UpgradeScript]) -> dict:
44
44
 
45
45
  if len(result) != len(data):
46
46
  unresolved = set(data) - set(result)
47
- echo(f"Warning: Dependency cycle detected or unresolved dependencies in: {unresolved}", err=True)
47
+ cli_message(f"Warning: Dependency cycle detected or unresolved dependencies in: {unresolved}")
48
48
  # Extend results, potentially might cause errors, because of cycles
49
49
  result.extend(unresolved)
50
50
 
@@ -1,8 +1,6 @@
1
1
  from contextlib import ExitStack
2
2
  from typing import Generator
3
3
 
4
- from alembic.operations import Operations
5
- from alembic.runtime.migration import MigrationContext
6
4
  from click import echo
7
5
  from sqlalchemy.engine import Inspector
8
6
  from tqdm import tqdm
@@ -105,6 +103,9 @@ def gather_associated_tables(backend: PostgreSQL, table_identifier: TableIdentif
105
103
 
106
104
 
107
105
  def migrate_schemas(context: Context, **kwargs):
106
+ from alembic.operations import Operations
107
+ from alembic.runtime.migration import MigrationContext
108
+
108
109
  ensure_store_is_loaded(context)
109
110
  store = context.get("store")
110
111
 
spinta/components.py CHANGED
@@ -1095,7 +1095,8 @@ 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
+ default_access_level: Access
1099
+ access: Access
1099
1100
  http_basic_auth: bool
1100
1101
  token_validation_key: dict | None = None
1101
1102
  token_validation_keys_download_url: str | None = None
spinta/config.py CHANGED
@@ -344,7 +344,8 @@ CONFIG = {
344
344
  },
345
345
  "config_path": pathlib.Path("tests/config"),
346
346
  "default_auth_client": "baa448a8-205c-4faa-a048-a10e4b32a136",
347
- "default_access_level": "protected",
347
+ "default_access_level": "open",
348
+ "access": "open",
348
349
  "sync_retry_count": 0,
349
350
  },
350
351
  },
@@ -164,7 +164,7 @@ def getall(
164
164
  builder.update(model=model, params={param.name: param for param in resource.params}, url_query_params=query)
165
165
 
166
166
  props = {}
167
- for prop in model.properties.values():
167
+ for prop in model.flatprops.values():
168
168
  if prop.external and prop.external.name:
169
169
  root_source = _get_prop_full_source(model.external.name, prop.external.name)
170
170
  props[prop.external.name] = {
@@ -21,7 +21,9 @@ from spinta.typing import ObjectData
21
21
  from spinta.ufuncs.querybuilder.components import QueryParams
22
22
 
23
23
 
24
- def _get_data_soap(url: str, backend: Soap, soap_request_body: dict, extra_headers: dict) -> list[dict]:
24
+ def _get_data_soap(
25
+ url: str, backend: Soap, soap_request_body: dict, extra_headers: dict, source: str | None = None
26
+ ) -> list[dict]:
25
27
  for key, value in soap_request_body.items():
26
28
  if isinstance(value, MakeCDATA):
27
29
  soap_request_body[key] = value()
@@ -38,6 +40,15 @@ def _get_data_soap(url: str, backend: Soap, soap_request_body: dict, extra_heade
38
40
  except zeep.exceptions.Error as e:
39
41
  raise UnexpectedErrorReadingData(exception=type(e).__name__, message=str(e))
40
42
 
43
+ if isinstance(response_data, dict):
44
+ for part in source.split("/"):
45
+ if not part:
46
+ continue
47
+ if part in response_data:
48
+ response_data = response_data[part]
49
+ else:
50
+ response_data = []
51
+ break
41
52
  if response_data and not isinstance(response_data, list):
42
53
  response_data = [response_data]
43
54
 
@@ -137,6 +148,7 @@ def getall(
137
148
  backend=backend,
138
149
  soap_request_body=builder.soap_request_body,
139
150
  extra_headers=http_headers,
151
+ source=model.external.name,
140
152
  )
141
153
  .flatten()
142
154
  .to_dataframe(meta=meta)
@@ -276,6 +276,11 @@ def dask_get_all(
276
276
  val = _get_row_value(context, row, sel, env.params)
277
277
  if sel.prop:
278
278
  if isinstance(sel.prop.dtype, PrimaryKey):
279
+ if isinstance(val, list):
280
+ val = [
281
+ list_value.get("_id", list_value) if isinstance(list_value, dict) else list_value
282
+ for list_value in val
283
+ ]
279
284
  val = keymap.encode(sel.prop.model.model_type(), val)
280
285
  elif isinstance(sel.prop.dtype, Ref):
281
286
  val = handle_ref_key_assignment(context, keymap, env, val, sel.prop.dtype)
@@ -336,12 +336,15 @@ def select(env: DaskDataFrameQueryBuilder, selected: Selected) -> Selected:
336
336
 
337
337
  @ufunc.resolver(DaskDataFrameQueryBuilder, Ref, GetAttr)
338
338
  def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: GetAttr) -> Selected | None:
339
- prep = env.call("select", prep)
340
- if prep is not None:
341
- return Selected(
342
- prop=dtype.prop,
343
- prep=prep,
344
- )
339
+ resolved_prep = env.call("select", prep)
340
+
341
+ result = {}
342
+ result["_id"] = Selected(prop=dtype.prop, prep=resolved_prep)
343
+ for prop in dtype.properties.values():
344
+ sel = env.call("select", prop)
345
+ result[prop.name] = sel
346
+
347
+ return Selected(prop=dtype.prop, prep=result)
345
348
 
346
349
 
347
350
  @ufunc.resolver(DaskDataFrameQueryBuilder, Ref, object)
@@ -353,6 +356,18 @@ def select(env: DaskDataFrameQueryBuilder, dtype: Ref, prep: Any) -> Selected:
353
356
  )
354
357
 
355
358
 
359
+ @ufunc.resolver(DaskDataFrameQueryBuilder, Ref)
360
+ def select(env: DaskDataFrameQueryBuilder, dtype: Ref) -> Selected:
361
+ prep = {}
362
+ prep["_id"] = Selected(item=dtype.prop.external.name, prop=dtype.prop)
363
+
364
+ for prop in dtype.properties.values():
365
+ sel = env.call("select", prop)
366
+ prep[prop.name] = sel
367
+
368
+ return Selected(prop=dtype.prop, prep=prep)
369
+
370
+
356
371
  @ufunc.resolver(DaskDataFrameQueryBuilder, GetAttr)
357
372
  def select(env: DaskDataFrameQueryBuilder, attr: GetAttr) -> Selected:
358
373
  resolved = env.resolve_property(attr)
@@ -14,6 +14,10 @@ from spinta.utils.schema import NA
14
14
 
15
15
 
16
16
  def handle_ref_key_assignment(context: Context, keymap: KeyMap, env: Env, value: Any, ref: Ref) -> dict:
17
+ original_value = value
18
+ if isinstance(value, dict):
19
+ value = value["_id"]
20
+
17
21
  keymap_name = ref.model.model_type()
18
22
  if ref.refprops != ref.model.external.pkeys:
19
23
  keymap_name = f"{keymap_name}.{'_'.join(prop.name for prop in ref.refprops)}"
@@ -55,7 +59,16 @@ def handle_ref_key_assignment(context: Context, keymap: KeyMap, env: Env, value:
55
59
 
56
60
  # FIXME Quick hack when trying to get `Internal` model keys while running in `External` mode (should probably return error, or None)
57
61
  if ref_model.mode == Mode.external and not check_if_model_has_backend_and_source(ref_model):
58
- return {"_id": keymap.encode(keymap_name, target_value)}
62
+ val = {"_id": keymap.encode(keymap_name, target_value)}
63
+ if isinstance(original_value, dict):
64
+ for nested_prop_name, nested_prop in ref.properties.items():
65
+ nested_value = original_value[nested_prop_name]
66
+ if isinstance(nested_value, dict):
67
+ nested_value = handle_ref_key_assignment(
68
+ context, keymap, env, nested_value, nested_prop.dtype
69
+ )
70
+ val[nested_prop_name] = nested_value
71
+ return val
59
72
 
60
73
  expr_parts = ["select()"]
61
74
  for i, prop in enumerate(ref.refprops):
@@ -67,7 +80,10 @@ def handle_ref_key_assignment(context: Context, keymap: KeyMap, env: Env, value:
67
80
  for row in rows:
68
81
  if val is not None:
69
82
  raise MultiplePrimaryKeyCandidatesFound(ref, values=target_value)
70
- val = row["_id"]
83
+ if "_id" in row:
84
+ val = row["_id"]
85
+ else:
86
+ val = keymap.encode(keymap_name, target_value)
71
87
  found_value = True
72
88
 
73
89
  if not found_value:
@@ -84,6 +100,13 @@ def handle_ref_key_assignment(context: Context, keymap: KeyMap, env: Env, value:
84
100
  values = values[0]
85
101
  val[prop] = values
86
102
  i = i + count
103
+
104
+ if isinstance(original_value, dict):
105
+ for nested_prop_name, nested_prop in ref.properties.items():
106
+ nested_value = original_value[nested_prop_name]
107
+ if isinstance(nested_value, dict):
108
+ nested_value = handle_ref_key_assignment(context, keymap, env, nested_value, nested_prop.dtype)
109
+ val[nested_prop_name] = nested_value
87
110
  return val
88
111
 
89
112
 
spinta/exceptions.py CHANGED
@@ -1223,3 +1223,9 @@ class SourceOrPrepareNotAllowed(UserError):
1223
1223
  template = """
1224
1224
  The source {source} was not expected. Delete it from the manifest or update the prepare function to allow it.
1225
1225
  """
1226
+
1227
+
1228
+ class PartialIncorrectProperty(BaseError):
1229
+ template = (
1230
+ "The composite property {property} is not correct. Check if all parts of the composite property are present."
1231
+ )
spinta/testing/client.py CHANGED
@@ -94,7 +94,6 @@ def create_remote_server(
94
94
  {
95
95
  "config_path": confdir,
96
96
  "default_auth_client": None,
97
- "default_access_level": "protected",
98
97
  }
99
98
  )
100
99
  context = create_test_context(rc)
@@ -294,7 +293,6 @@ def create_rc(
294
293
  },
295
294
  # tests/config/clients/3388ea36-4a4f-4821-900a-b574c8829d52.yml
296
295
  "default_auth_client": "3388ea36-4a4f-4821-900a-b574c8829d52",
297
- "default_access_level": "protected",
298
296
  }
299
297
  )
300
298
 
spinta/testing/csv.py CHANGED
@@ -1,8 +1,13 @@
1
- from typing import List
1
+ import pathlib
2
2
 
3
3
  from requests.models import Response
4
4
 
5
5
 
6
- def parse_csv(resp: Response) -> List[List[str]]:
6
+ def parse_csv(resp: Response) -> list[list[str]]:
7
7
  resp.raise_for_status()
8
8
  return [line.strip().split(",") for line in resp.text.splitlines()]
9
+
10
+
11
+ def read_csv(path: pathlib.Path) -> list[list[str]]:
12
+ with path.open("r") as f:
13
+ return [line.strip().split(",") for line in f.readlines()]
spinta/types/config.py CHANGED
@@ -83,6 +83,11 @@ def load(context: Context, config: Config) -> Config:
83
83
  default="private",
84
84
  cast=lambda name: get_enum_by_name(Access, name),
85
85
  )
86
+ config.access = rc.get(
87
+ "access",
88
+ default="open",
89
+ cast=lambda name: get_enum_by_name(Access, name),
90
+ )
86
91
  config.http_basic_auth = rc.get("http_basic_auth", default=False, cast=asbool)
87
92
  config.token_validation_key = rc.get("token_validation_key", cast=json.loads) or None
88
93
  config.token_validation_keys_download_url = rc.get("token_validation_keys_download_url")
@@ -2,23 +2,29 @@ from copy import copy
2
2
 
3
3
  from spinta import commands
4
4
  from spinta.components import Context, Property
5
- from spinta.exceptions import PartialTypeNotFound, ParentNodeNotFound
5
+ from spinta.core.enums import Mode
6
+ from spinta.exceptions import PartialTypeNotFound, ParentNodeNotFound, PartialIncorrectProperty
6
7
  from spinta.types.datatype import Partial, Ref
7
8
 
8
9
 
9
- def get_ref_value(context: Context, prop: Property):
10
+ def get_ref_value(context: Context, prop: Property) -> Property | None:
10
11
  parent = prop.parent
11
- if isinstance(parent, Property) and isinstance(parent.dtype, Ref):
12
- parent_parent = parent.parent
13
- if isinstance(parent_parent, Property) and isinstance(parent_parent.dtype, Ref):
14
- model = parent_parent.dtype.model
15
- first_level = model.properties[parent.name]
16
- if prop.name in first_level.dtype.properties:
17
- return first_level.dtype.properties[prop.name]
18
- else:
19
- model = parent.dtype.model
20
- if prop.name in model.properties:
21
- return model.properties[prop.name]
12
+ if not (isinstance(parent, Property) and isinstance(parent.dtype, Ref)):
13
+ return None
14
+ if isinstance(parent.dtype.model, str):
15
+ parent.dtype.model = commands.get_model(context, prop.model.manifest, parent.dtype.model)
16
+ model = parent.dtype.model
17
+
18
+ if prop.name not in model.properties:
19
+ return None
20
+
21
+ model_property_value = model.properties[prop.name]
22
+ model_dtype = model_property_value.dtype
23
+ if hasattr(model_dtype, "model") and isinstance(model_dtype.model, str):
24
+ model_property_value.dtype.model = commands.get_model(
25
+ context, prop.model.manifest, model_property_value.dtype.model
26
+ )
27
+ return model_property_value
22
28
 
23
29
 
24
30
  @commands.link.register(Context, Partial)
@@ -29,14 +35,23 @@ def link(context: Context, dtype: Partial):
29
35
  if isinstance(parent.dtype, Ref):
30
36
  props = dtype.properties
31
37
  result = get_ref_value(context, prop)
38
+ if not result:
39
+ raise PartialIncorrectProperty(dtype)
32
40
  prop.dtype = copy(result.dtype)
33
41
  prop.dtype.properties = props
34
42
  prop.dtype.inherited = True
35
43
  prop.given.explicit = False
36
44
  prop.given.name = ""
37
45
  prop.dtype.prop = prop
38
- for partial_prop in props.values():
39
- commands.link(context, partial_prop)
46
+ if prop.level is None:
47
+ prop.level = result.level
48
+ # For external mode copy the external mapping
49
+ # For internal mode leave it as is, because it breaks multi-level denormalization
50
+ if result.external and prop.model.mode == Mode.external:
51
+ prop.external = copy(result.external)
52
+ if isinstance(prop.dtype, Ref):
53
+ prop.dtype.refprops = []
54
+ commands.link(context, prop.dtype)
40
55
  else:
41
56
  raise PartialTypeNotFound(dtype)
42
57
  else:
spinta/types/ref/link.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import List
2
2
 
3
3
  from spinta import commands
4
- from spinta.components import Context
4
+ from spinta.components import Context, Model
5
5
  from spinta.types.datatype import Ref
6
6
  from spinta.exceptions import ModelReferenceNotFound, MissingRefModel
7
7
  from spinta.exceptions import ModelReferenceKeyNotFound
@@ -25,6 +25,8 @@ def link(context: Context, dtype: Ref) -> None:
25
25
  # Self reference.
26
26
  dtype.model = dtype.prop.model
27
27
  else:
28
+ if isinstance(rmodel, Model):
29
+ rmodel = rmodel.name
28
30
  if not commands.has_model(context, dtype.prop.model.manifest, rmodel):
29
31
  raise ModelReferenceNotFound(dtype, ref=rmodel)
30
32
  dtype.model = commands.get_model(context, dtype.prop.model.manifest, rmodel)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spinta
3
- Version: 0.2.dev21
3
+ Version: 0.2.dev23
4
4
  Summary: A platform for describing, extracting, transforming, loading and serving open data.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -12,7 +12,7 @@ spinta/api/inspect.py,sha256=sf3Q8gUEqqCusPCiT1ZqH4Kyc7T9voKWWVRlQXSoIU0,6776
12
12
  spinta/api/schema.py,sha256=rsWeKxk6JigoawD56pb8n8AYve59YIrKQyVrqpUsX9I,7634
13
13
  spinta/api/validators.py,sha256=GcdKAq-3n9oe58VLClNOBXo3MSNyguCtgWqracHR_9w,666
14
14
  spinta/asgi.py,sha256=Too66Cvo4UX0a05K-SSVRUrj8NEBVYpaql_1sq3d8gw,530
15
- spinta/auth.py,sha256=gCFAuCMBkOkV7NzPr5ckCzaNSyryYF3QJQE-PLpkjCM,42550
15
+ spinta/auth.py,sha256=4QmNrZpVrjaVU3E9g2OWWMkI_4pt7l0JXHW3JA6iXS0,42587
16
16
  spinta/backends/__init__.py,sha256=i8zlodXE0ku_N38EBoDoNXvLvBWvc_AIm5SGHSP3aAI,56972
17
17
  spinta/backends/components.py,sha256=4nV3Yvye2rYAnBe0PV-rPuK2IXnHzrUFm37ceGI47eo,1663
18
18
  spinta/backends/constants.py,sha256=RdmpPhIIUsSwO6sQv1eZdmdl6Rf1h_3qAHDUcTOYOy0,828
@@ -173,18 +173,19 @@ spinta/backends/postgresql/ufuncs/result/ufuncs.py,sha256=n96VPXwU0vxS8lZu-8s_zK
173
173
  spinta/backends/postgresql/ufuncs/ufuncs.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
174
174
  spinta/backends/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
175
175
  spinta/cli/__init__.py,sha256=3DWdLNWcfRk0mVZPwqKhQfRc47w716jhFjHLXSFTH-c,21
176
- spinta/cli/admin.py,sha256=HT_aEn4tqUXckKWUXPV8VEoMXe1ZW3strNGoEimCd9A,2902
176
+ spinta/cli/admin.py,sha256=FYFiMjWTcRGYZiS3GYcbF8h16XuYuq63VI9qToDGXP0,3123
177
177
  spinta/cli/auth.py,sha256=WDwn7p5JDwTq3xS9pki3xykmRrd1D7VxRkyv7nGjvIY,6036
178
178
  spinta/cli/config.py,sha256=U8WjKkHo9B0TczZuZLUhWmoCyh5YI8EW5loTqFJyGao,1736
179
179
  spinta/cli/data.py,sha256=nOlVj2GEZIvs0oRjTRibmlR0DlLh-GUs3nefZVrLS0c,8288
180
180
  spinta/cli/get.py,sha256=GUzj7hg3X0W06EIARveLL93vxKTWfVAUNvtQfmcxepA,1191
181
181
  spinta/cli/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
182
  spinta/cli/helpers/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
183
- spinta/cli/helpers/admin/components.py,sha256=4cE2vAJtqzm2w8ZPsyMAKMHYq3FBeugHms_ca1Yzu0U,307
184
- spinta/cli/helpers/admin/registry.py,sha256=yi_HJLpxd6KeyTmfLrN5ePmmTYZeHP1Wa-5BwCknw4o,1303
183
+ spinta/cli/helpers/admin/components.py,sha256=ir_CXA-r6GOdHx2nLT5Jiy-9vmxWj-6Ypz5xf27tbz0,335
184
+ spinta/cli/helpers/admin/registry.py,sha256=aJH9rJAeTfBaLadGE7x266TvBHSPhMIowCMjQRq8Ynw,1524
185
185
  spinta/cli/helpers/admin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
186
  spinta/cli/helpers/admin/scripts/changelog.py,sha256=r5heUIoGxILLUB2qqIpliN1JWYcGB6WmPfbVpOzWYm4,13771
187
187
  spinta/cli/helpers/admin/scripts/deduplicate.py,sha256=kn4vR7ZFC2-uihRIjYTY0en1BCGEM915mGE8IPI1K_8,9128
188
+ spinta/cli/helpers/admin/scripts/enums.py,sha256=OiNvg3KGLk5QnwgwyHp8nqoT5rf_d0XzX-Ma_f4yWco,5176
188
189
  spinta/cli/helpers/auth.py,sha256=qg35WJBCPHpQVLMOnvLKRqNcD6dSSX6NqldlauAuXBU,1196
189
190
  spinta/cli/helpers/data.py,sha256=T1t0X01beFthJQnlKFXWHMYxXjnZaPMxsbCP0wlJgHg,5530
190
191
  spinta/cli/helpers/errors.py,sha256=ILUm-sHIr4_bNlaonMuEaUJaPcYmOHz589M_sxpRzCo,412
@@ -198,7 +199,7 @@ spinta/cli/helpers/export/commands.py,sha256=obYO_6bXTfpJCkHn_fV9dsIwzhQwPQVUPhk
198
199
  spinta/cli/helpers/export/components.py,sha256=a5Q9qYym5KeZ0lyb5RXmKVND5EzQdf-PaeezzQMecXQ,1773
199
200
  spinta/cli/helpers/export/helpers.py,sha256=OpuPxrz3PNc9hKZROaavxdXbHsiiYoVXWWtxvETM0II,4737
200
201
  spinta/cli/helpers/manifest.py,sha256=8R39le_edqfXnC1ar_Q79JgEz05gBko64SarOjSixBs,1317
201
- spinta/cli/helpers/message.py,sha256=sSKYNeDp0rql7fKuheYU9xAJX4g-sfaUJahpcLjnP1g,317
202
+ spinta/cli/helpers/message.py,sha256=EwcZtC4JkUFsXcNmw6URfqPR2TQMLTIBD6EvfojMZ4M,518
202
203
  spinta/cli/helpers/migrate.py,sha256=fhd4tra_jqYyEEMbh-2GBXQ3d_RGWt3HSfAmvSimiPo,460
203
204
  spinta/cli/helpers/push/__init__.py,sha256=KEllTU28EFUKU9qGagCeaT6E49zXSb6HTQYzN6G8NAY,2141
204
205
  spinta/cli/helpers/push/components.py,sha256=FeAPb4dOxv4Kok_Sf-3hpqbiHLPeyJ4J7r-D5oKHxao,2235
@@ -210,9 +211,9 @@ spinta/cli/helpers/push/sync.py,sha256=dSH5TuDTQwbl5qaVLGW5a4vV1eiq2Yo8dB4huRCKM
210
211
  spinta/cli/helpers/push/utils.py,sha256=Jc9TNaosbYp2TecumSVDSP59EDsdcoUfXwAihV4xI30,5846
211
212
  spinta/cli/helpers/push/write.py,sha256=mqKDHq_naooWGW_okpyj4jKjutwF7Gt3ssLhGB3mhyA,13563
212
213
  spinta/cli/helpers/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
- spinta/cli/helpers/script/components.py,sha256=mhUq2MG64TMY88HWhwwT9yvGKyo4bYR2W1LW8k9ot_k,4418
214
- spinta/cli/helpers/script/core.py,sha256=U41UigkfrPeSWZsPAK7SVXiBAsSeQzcSQsWMWpUtpOM,3717
215
- spinta/cli/helpers/script/helpers.py,sha256=ILjTrUknrL_bBHRtf57Y4PQr-m6VpS3zfaaaT5e_-1M,2201
214
+ spinta/cli/helpers/script/components.py,sha256=OSmIuQ7wG20vdgYoRZKrkiHkw4YM8bV5puV7EdP348k,4417
215
+ spinta/cli/helpers/script/core.py,sha256=KltI0rHaowz__j5AbpTPTlVc-j3SAiGarJwGPyO1jEM,3716
216
+ spinta/cli/helpers/script/helpers.py,sha256=ubx9BhhrPqEWIVrYC2LUTgwILI3I1WDGd8e68RIBzkU,2226
216
217
  spinta/cli/helpers/script/registry.py,sha256=h-wPrE7fOCD7B9hmqWTHNPHf1GyuEZuvJd9VmlB5khE,3266
217
218
  spinta/cli/helpers/store.py,sha256=LLXQtbuU0tjBB3f1DmWqmy57htY3pjog4tmJZDu9IrA,5507
218
219
  spinta/cli/helpers/sync/__init__.py,sha256=zarobjW3JbLgtYl9TWKusrFyzten5qRbwDo3447Kyrc,140
@@ -235,7 +236,7 @@ spinta/cli/helpers/upgrade/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
235
236
  spinta/cli/helpers/upgrade/scripts/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
236
237
  spinta/cli/helpers/upgrade/scripts/backends/postgresql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
237
238
  spinta/cli/helpers/upgrade/scripts/backends/postgresql/comments.py,sha256=ZqbbY0yTgS1p46N_Fn-RDsWxYdgslIwqstcEczqTxGI,9239
238
- spinta/cli/helpers/upgrade/scripts/backends/postgresql/schemas.py,sha256=6CwhML-E8WCFQpzBiFPLmsGfrYgBWOPA5AVEcXe44pE,11522
239
+ spinta/cli/helpers/upgrade/scripts/backends/postgresql/schemas.py,sha256=lUoeup6OumGb9A_8iTMVctyBQrza7jsufuZXWkcaLxs,11531
239
240
  spinta/cli/helpers/upgrade/scripts/clients.py,sha256=-OrfJvJZ4e07TyYpE7ltPoqntBgs_21J2TuzH6ukYVQ,7420
240
241
  spinta/cli/helpers/upgrade/scripts/keymaps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
241
242
  spinta/cli/helpers/upgrade/scripts/keymaps/sqlalchemy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -266,8 +267,8 @@ spinta/commands/search.py,sha256=Yn-lxFhAQrDzDWFGlTm_WmQmUFXsu6NhuT7PpNJ67Ls,311
266
267
  spinta/commands/version.py,sha256=rZGszGCU_cgyJyRUgZgg_ts3ZldqemLcugpOOY_UJ64,745
267
268
  spinta/commands/write.py,sha256=en2dcD1nb2T28cQDMa41qbeIhZEVH2B1xPDP-nG8mBs,42398
268
269
  spinta/compat.py,sha256=eI-tsNtWww_rEW6Gw7tcgoLvUMOb_f1vV-kLjzFPRMs,2919
269
- spinta/components.py,sha256=jLgIFxMO6gwol-yxHmKGSWNhGs2qF99SbDvzLjwG1to,35373
270
- spinta/config.py,sha256=xrkMwNo_XrtUZLmXMS303-Pqcjoo0PfV7gT8wOOf4xk,16247
270
+ spinta/components.py,sha256=uZ6VFzonu0MolUN2lYSrl_Ggsaoak6cNP5VcZ1BbtMc,35395
271
+ spinta/config.py,sha256=YR4Cxt1ceg-u9B4lZxOYKuAeGrsqiJKKA9pTffppn4A,16272
271
272
  spinta/config.yml,sha256=RvUjqZt-EL5wnWaizmoz1b85aBz43NhQwK0fB9WWoHA,3447
272
273
  spinta/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
273
274
  spinta/core/access.py,sha256=EAfG-5hMmhvHdCRSZ65z1y_n6-8i-X-ISElMbR9MIxw,2247
@@ -285,13 +286,13 @@ spinta/datasets/backends/dataframe/backends/csv/commands/read.py,sha256=4klYZKtG
285
286
  spinta/datasets/backends/dataframe/backends/csv/components.py,sha256=0EZvqouPeaPq3sHtzSGX9HW4eLtMMnRjAlMNYlu4cAI,209
286
287
  spinta/datasets/backends/dataframe/backends/json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
287
288
  spinta/datasets/backends/dataframe/backends/json/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
288
- spinta/datasets/backends/dataframe/backends/json/commands/read.py,sha256=WfU0R5dmlyI2ETmGzNe34oBNwN0uzo5HI4YQXioC4Vk,7119
289
+ spinta/datasets/backends/dataframe/backends/json/commands/read.py,sha256=pjSsUEbZiCeXs02qFDoicfpJFSAFhB81qp4Lps9yt0U,7118
289
290
  spinta/datasets/backends/dataframe/backends/json/components.py,sha256=kPKQEEvWGZKq_IpiwhVNG4A-7CaEux5fGEuwwcaHCLk,211
290
291
  spinta/datasets/backends/dataframe/backends/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
291
292
  spinta/datasets/backends/dataframe/backends/memory/components.py,sha256=tByPlAR0hp8WsL0i5JxAPbOh7bdawlw9UQEfT5SUIEo,226
292
293
  spinta/datasets/backends/dataframe/backends/soap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
293
294
  spinta/datasets/backends/dataframe/backends/soap/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
294
- spinta/datasets/backends/dataframe/backends/soap/commands/read.py,sha256=BXdfQPkgayP35YyAuTJk8JZCD6LKgqc8dSrSBvkKjUo,5300
295
+ spinta/datasets/backends/dataframe/backends/soap/commands/read.py,sha256=5pVdcSQ47QH9RXgu6_sw8E0eMLW9colZ-6AgeWwnh_I,5667
295
296
  spinta/datasets/backends/dataframe/backends/soap/components.py,sha256=TtJFhUfJ6MZTbCUxQC_yiADEGPL8uGVVX9nDVrGiJoA,1451
296
297
  spinta/datasets/backends/dataframe/backends/soap/ufuncs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
297
298
  spinta/datasets/backends/dataframe/backends/soap/ufuncs/components.py,sha256=VmKOHUeeCBcYz3xSh6qj657VeU7BgBL6X8DoSLmEMLE,886
@@ -302,15 +303,15 @@ spinta/datasets/backends/dataframe/backends/xml/commands/read.py,sha256=dqb_8lL_
302
303
  spinta/datasets/backends/dataframe/backends/xml/components.py,sha256=CcqTkD2MWky4R_wuY588nUnuUrzagBWTctHa5kV47L4,209
303
304
  spinta/datasets/backends/dataframe/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
305
  spinta/datasets/backends/dataframe/commands/check.py,sha256=eghehENfCjCcGeCXGzPXRXczQenV7F5j9T1xuDVUx7c,1385
305
- spinta/datasets/backends/dataframe/commands/read.py,sha256=C0f1GZoM6btgyOV_NUBdTa12Dw7jcth8E6_APHYJ1IA,10831
306
+ spinta/datasets/backends/dataframe/commands/read.py,sha256=kzQu-MD21H5UaA2fDIt4wLEb-ukfTevW6spk6vhkQGY,11095
306
307
  spinta/datasets/backends/dataframe/components.py,sha256=cnlB1VlZrdtVo4-8q7LukyAvsUr9l2SD-R0PzqJSoMQ,235
307
308
  spinta/datasets/backends/dataframe/ufuncs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
308
309
  spinta/datasets/backends/dataframe/ufuncs/components.py,sha256=gmfS7P6gMyPbjJf2ZY-zWRySXGaIxbIPIqar6p9ARh8,90
309
310
  spinta/datasets/backends/dataframe/ufuncs/query/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
310
311
  spinta/datasets/backends/dataframe/ufuncs/query/components.py,sha256=uekQHMdpJi5506-sWH-HakCw_kGS_0BN5A5Ym1i4yAs,2342
311
- spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py,sha256=LQs10MMEIzQ4bX-mikHSpOFnME2EwhHpOrhwRZzDNJw,19023
312
+ spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py,sha256=LUCL7rAqM1unZar0BwBH0oIA-FLt-L4E4_J9nBhlyiE,19537
312
313
  spinta/datasets/backends/dataframe/ufuncs/ufuncs.py,sha256=LRy_jE48FYtW41-oj4G35ihA70P39qCGWfbgS7amv9U,391
313
- spinta/datasets/backends/helpers.py,sha256=yjoi5mx_UsY62wRcmSKT2zsM_908LVPCJRmyxXsYefE,6557
314
+ spinta/datasets/backends/helpers.py,sha256=2UZGZfEsb45jpsLRFmeDhMTuXBk9DX2cAqo_oD4jL3U,7689
314
315
  spinta/datasets/backends/notimpl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
315
316
  spinta/datasets/backends/notimpl/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
316
317
  spinta/datasets/backends/notimpl/commands/load.py,sha256=5wbxy7LJ61fBSM2YpSWqHRFWh2-OQgsG9SUiOW4zCzc,372
@@ -460,7 +461,7 @@ spinta/dimensions/prefix/commands/load.py,sha256=P8Lzd7smS-lJ30NTFBPs_2ywmZw2qlr
460
461
  spinta/dimensions/prefix/components.py,sha256=5VJel2wvZUCol_4riD119CD5F5R6TSdf5ISeWDglKsk,618
461
462
  spinta/dimensions/prefix/helpers.py,sha256=VE6TT50tqyy-9becV2PV_TMjA4HXGrSSidV4F7aUCxE,1003
462
463
  spinta/dispatcher.py,sha256=FZaBJ7O2J-0hZ4fGL2w3a_cO8jzOVuxqyfOqON6jg0E,3769
463
- spinta/exceptions.py,sha256=vKbaYqFmF-XgS2yMe7jabt_cLaq2YBI9f_JUum8merg,35860
464
+ spinta/exceptions.py,sha256=6KPBa-zLMyhz-focdNypsnTDyTXFCGzh4ShmPLM8Ohc,36046
464
465
  spinta/fetcher.py,sha256=55-nxQy9WuISrLAPuJOxjJjtZ0opqTlPiCHI7iABiic,1330
465
466
  spinta/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
466
467
  spinta/formats/ascii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -611,10 +612,10 @@ spinta/templates/error.html,sha256=nlC52cRicXqs1nxdwl_4ikMZA9-N9WKp1-OLOp_oJwA,1
611
612
  spinta/templates/form.html,sha256=Ioszr7D8v_-ZkwmEHNeeMyHnh6p60te0wMDkKwdDaCA,794
612
613
  spinta/testing/__init__.py,sha256=bXkhOgOOugNFKv6O6uHrANccQ98B4E352WeBtJDUZ4E,50
613
614
  spinta/testing/cli.py,sha256=HPwUHeo2W5q5dRl56kIytJIxvG7WTdi52N_CMylEuvM,1840
614
- spinta/testing/client.py,sha256=XdTF4J36GVpM-HXc1b6FG_TY4-lB5PyXNmKEPvJD2ls,10138
615
+ spinta/testing/client.py,sha256=q_SLseq_hLN7O8-pxWJmNfJqeoMCGQrfWPy8Fh-oqQ8,10040
615
616
  spinta/testing/config.py,sha256=o40GCI0qAeQGK1u-VrxtnUh45m0o-tPHySJ0oQmN4j4,2919
616
617
  spinta/testing/context.py,sha256=9lDGKEN1wV58TW9KlrJyuzCo2vMer_HSP2MKQsKPZqI,3382
617
- spinta/testing/csv.py,sha256=XxAXwlQLd9yHmNi7m3oClPJJcDCnMX2qqOAz4WrAbgA,214
618
+ spinta/testing/csv.py,sha256=JnrB3xwQDvd0EL7fQN9CcvLs9Bspydq0mdqAE11QlPM,357
618
619
  spinta/testing/data.py,sha256=YHIypRRy1InkFwgx-jvLUaCCAvY1rA-QeHPYn4bL1iE,7259
619
620
  spinta/testing/datasets.py,sha256=gzPQoq_1XEzN1v9ghESNEFN5GPrwE622uifObDaBKMs,1478
620
621
  spinta/testing/dtypes.py,sha256=5qGBuCjDJywTJSvJmvA62DvY8H-dJFti441wtGkrvO0,6288
@@ -636,7 +637,7 @@ spinta/types/array/resolve.py,sha256=o8fEPK0RZx5FT6oMQ2RZyE8WYT3WK9qdh6QTORzndD8
636
637
  spinta/types/backref/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
637
638
  spinta/types/backref/link.py,sha256=_q7wHvuRUuGpllth_zTerq6Eoajq3C_WqjsqjxZTc7c,4684
638
639
  spinta/types/command.py,sha256=pmlL4ZQYFwzNm0nMY4q7GZIoPNqiYxZ-k6CzfTByCg4,975
639
- spinta/types/config.py,sha256=wm-kFU15eIkOI0ZyOrDQgCrQv_5J5QT8uFXk7BJbO8Q,6438
640
+ spinta/types/config.py,sha256=Yl9oGiE3znHZgtHQNtr8s7BGARCV_O10ibe1Mp8orss,6572
640
641
  spinta/types/datatype.py,sha256=zbarMo6S9MX__NHfnekw2Oto3MGqj3X4Ms5tWGodjps,16747
641
642
  spinta/types/denorm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
642
643
  spinta/types/denorm/check.py,sha256=2lZrDwd_F71hkflF7jvgFp_vyPRtAQV3HVlFZAEodwY,606
@@ -659,10 +660,10 @@ spinta/types/object/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
659
660
  spinta/types/object/check.py,sha256=14D_lGNIsZvNevQ9FhRTKtGGK3VJA9ugCI83xg4QYX4,276
660
661
  spinta/types/object/link.py,sha256=wjtzkVcS4gN-9etAauj2sLJczOi8Gwig-WpVaSFu_aA,361
661
662
  spinta/types/partial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
662
- spinta/types/partial/link.py,sha256=VusV7jenKC6LZNj7gFBp2ibmWQiBKP-D66UvQUv6JMY,1823
663
+ spinta/types/partial/link.py,sha256=G4ieydtTOJloGPqB7VHpjmyZ3BIKieA-I1ROX3cvyw0,2487
663
664
  spinta/types/ref/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
664
665
  spinta/types/ref/check.py,sha256=I8gFHJiuYWGw1UIlGx2vyxlqExt15EkJ1jPz409fLXs,853
665
- spinta/types/ref/link.py,sha256=b3tahnT5VGq2Rg1qEyL5r56r6HAFeBubXCNkp6-AJbY,2008
666
+ spinta/types/ref/link.py,sha256=Cuu1E6iUYBcGXLYYm814ptlBe6lXNWr-QvBDGQm1X8U,2086
666
667
  spinta/types/ref/resolve.py,sha256=X3J5etLCaBIWfp0saqBIb0XOS7DkpM8-tFhVUvlN8OQ,309
667
668
  spinta/types/store.py,sha256=6dVzeIUoYZC-1F-LVA4ZSG23t593twvRY0-aa1-RWiY,5234
668
669
  spinta/types/text/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -740,8 +741,8 @@ spinta/utils/tree.py,sha256=iF8eOSBagUoDmdJGNQgsYB_gshsFajmiUALXqU9luHE,591
740
741
  spinta/utils/types.py,sha256=lfYSxKGPuPeUsO14d2OYodtbRY3zsa-o-z8HveVH3t0,801
741
742
  spinta/utils/units.py,sha256=CFFLv1NHYsoSSzwiar3zXYmt4m3sccW5niUgkZQgo3k,747
742
743
  spinta/utils/url.py,sha256=b6sqQEpmCdT3oV4vGDzXnN8w415InAYjIW_o2djhQS8,2950
743
- spinta-0.2.dev21.dist-info/METADATA,sha256=aPvAeY7zuGWVoo5Uhv28-SZHYJkW1Y5nIvP4wWuJHfQ,10214
744
- spinta-0.2.dev21.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
745
- spinta-0.2.dev21.dist-info/entry_points.txt,sha256=-jdsOQZcMu3rUOwgIJNS3gZS4rwWPACuXXy128F676w,46
746
- spinta-0.2.dev21.dist-info/licenses/LICENSE,sha256=JKmjfBLapeFWNI_qdVr5bXGlsuMPa6nRarKPK5davKw,1071
747
- spinta-0.2.dev21.dist-info/RECORD,,
744
+ spinta-0.2.dev23.dist-info/METADATA,sha256=InX-K2DKF6mZUaQ5Kn7jEHA58o2JHRM38T1o_sM5ptU,10214
745
+ spinta-0.2.dev23.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
746
+ spinta-0.2.dev23.dist-info/entry_points.txt,sha256=-jdsOQZcMu3rUOwgIJNS3gZS4rwWPACuXXy128F676w,46
747
+ spinta-0.2.dev23.dist-info/licenses/LICENSE,sha256=JKmjfBLapeFWNI_qdVr5bXGlsuMPa6nRarKPK5davKw,1071
748
+ spinta-0.2.dev23.dist-info/RECORD,,