spinta 0.2.dev22__py3-none-any.whl → 0.2.dev24__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/backends/__init__.py +60 -2
- spinta/backends/helpers.py +26 -1
- spinta/cli/admin.py +12 -8
- spinta/cli/comment.py +113 -0
- spinta/cli/helpers/admin/components.py +1 -0
- spinta/cli/helpers/admin/registry.py +4 -0
- spinta/cli/helpers/admin/scripts/enums.py +148 -0
- spinta/cli/helpers/enums.py +5 -0
- spinta/cli/helpers/message.py +6 -2
- spinta/cli/helpers/script/components.py +1 -1
- spinta/cli/helpers/script/core.py +5 -7
- spinta/cli/helpers/script/helpers.py +2 -2
- spinta/cli/main.py +4 -0
- spinta/cli/manifest.py +11 -9
- spinta/cli/uncomment.py +110 -0
- spinta/components.py +12 -0
- spinta/config.py +1 -0
- spinta/core/access.py +2 -0
- spinta/datasets/backends/dataframe/backends/soap/commands/read.py +13 -1
- spinta/datasets/backends/dataframe/commands/read.py +5 -2
- spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py +11 -1
- spinta/datasets/backends/helpers.py +2 -1
- spinta/datasets/backends/sql/commands/cast.py +20 -17
- spinta/datasets/backends/sql/commands/read.py +50 -15
- spinta/datasets/backends/sql/ufuncs/query/ufuncs.py +11 -1
- spinta/datasets/components.py +0 -1
- spinta/datasets/helpers.py +36 -3
- spinta/dimensions/comments/components.py +3 -0
- spinta/dimensions/comments/helpers.py +2 -0
- spinta/dimensions/scope/__init__.py +0 -0
- spinta/dimensions/scope/components.py +46 -0
- spinta/dimensions/scope/helpers.py +39 -0
- spinta/exceptions.py +24 -5
- spinta/formats/html/commands.py +8 -5
- spinta/formats/html/helpers.py +7 -1
- spinta/manifests/internal_sql/helpers.py +4 -2
- spinta/manifests/mermaid/helpers.py +251 -180
- spinta/manifests/tabular/components.py +19 -0
- spinta/manifests/tabular/helpers.py +110 -6
- spinta/testing/csv.py +7 -2
- spinta/types/__init__.py +1 -0
- spinta/types/array/__init__.py +1 -0
- spinta/types/array/link.py +6 -3
- spinta/types/backref/__init__.py +1 -0
- spinta/types/backref/link.py +9 -3
- spinta/types/datatype.py +11 -0
- spinta/types/helpers.py +67 -2
- spinta/types/model.py +44 -2
- spinta/types/ref/__init__.py +1 -0
- spinta/types/ref/link.py +5 -3
- spinta/urlparams.py +2 -0
- spinta/utils/naming.py +2 -2
- spinta/utils/url.py +3 -0
- {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/METADATA +2 -1
- {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/RECORD +58 -51
- {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/WHEEL +0 -0
- {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/entry_points.txt +0 -0
- {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/licenses/LICENSE +0 -0
spinta/backends/__init__.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Iterable
|
|
|
13
13
|
from typing import List
|
|
14
14
|
from typing import Optional
|
|
15
15
|
|
|
16
|
+
from cbor2 import dumps as cbor_dumps
|
|
16
17
|
import dateutil
|
|
17
18
|
import shapely.geometry.base
|
|
18
19
|
from geoalchemy2.elements import WKTElement, WKBElement
|
|
@@ -23,7 +24,7 @@ from spinta import commands
|
|
|
23
24
|
from spinta import exceptions
|
|
24
25
|
from spinta.backends.components import Backend
|
|
25
26
|
from spinta.backends.components import SelectTree
|
|
26
|
-
from spinta.backends.helpers import check_unknown_props, get_select_tree, prepare_response
|
|
27
|
+
from spinta.backends.helpers import check_unknown_props, get_select_tree, prepare_response, is_accessible_by_equals_sign
|
|
27
28
|
from spinta.backends.helpers import flat_select_to_nested
|
|
28
29
|
from spinta.backends.helpers import get_model_reserved_props
|
|
29
30
|
from spinta.backends.helpers import get_select_prop_names
|
|
@@ -58,7 +59,19 @@ from spinta.exceptions import (
|
|
|
58
59
|
from spinta.exceptions import NoItemRevision
|
|
59
60
|
from spinta.formats.components import Format
|
|
60
61
|
from spinta.manifests.components import Manifest
|
|
61
|
-
from spinta.types.datatype import
|
|
62
|
+
from spinta.types.datatype import (
|
|
63
|
+
Array,
|
|
64
|
+
ExternalRef,
|
|
65
|
+
Inherit,
|
|
66
|
+
PageType,
|
|
67
|
+
BackRef,
|
|
68
|
+
ArrayBackRef,
|
|
69
|
+
Integer,
|
|
70
|
+
Boolean,
|
|
71
|
+
Denorm,
|
|
72
|
+
Base32,
|
|
73
|
+
String,
|
|
74
|
+
)
|
|
62
75
|
from spinta.types.datatype import Binary
|
|
63
76
|
from spinta.types.datatype import DataType
|
|
64
77
|
from spinta.types.datatype import Date
|
|
@@ -590,12 +603,31 @@ def is_object_id(context: Context, value: str):
|
|
|
590
603
|
|
|
591
604
|
@is_object_id.register(Context, Backend, Model, str)
|
|
592
605
|
def is_object_id(context: Context, backend: Backend, model: Model, value: str):
|
|
606
|
+
return is_object_id(context, backend, model.properties["_id"].dtype, value)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@is_object_id.register(Context, Backend, PrimaryKey, str)
|
|
610
|
+
def is_object_id(context: Context, backend: Backend, dtype: PrimaryKey, value: str):
|
|
593
611
|
try:
|
|
594
612
|
return uuid.UUID(value).version == 4
|
|
595
613
|
except ValueError:
|
|
596
614
|
return False
|
|
597
615
|
|
|
598
616
|
|
|
617
|
+
@is_object_id.register(Context, Backend, DataType, str)
|
|
618
|
+
def is_object_id(context: Context, backend: Backend, dtype: DataType, value: str):
|
|
619
|
+
candidate = value
|
|
620
|
+
if is_accessible_by_equals_sign(dtype.prop, value):
|
|
621
|
+
if not value.startswith("="):
|
|
622
|
+
return False
|
|
623
|
+
candidate = value[1:]
|
|
624
|
+
try:
|
|
625
|
+
dtype.load(candidate)
|
|
626
|
+
except exceptions.InvalidValue:
|
|
627
|
+
return False
|
|
628
|
+
return True
|
|
629
|
+
|
|
630
|
+
|
|
599
631
|
@is_object_id.register(Context, Backend, Model, uuid.UUID)
|
|
600
632
|
def is_object_id(context: Context, backend: Backend, model: Model, value: uuid.UUID):
|
|
601
633
|
return value.version == 4
|
|
@@ -1750,6 +1782,13 @@ def cast_backend_to_python(context: Context, dtype: DataType, backend: Backend,
|
|
|
1750
1782
|
return data
|
|
1751
1783
|
|
|
1752
1784
|
|
|
1785
|
+
@commands.cast_backend_to_python.register(Context, String, Backend, object)
|
|
1786
|
+
def cast_backend_to_python(context: Context, dtype: String, backend: Backend, data: Any, **kwargs) -> Any:
|
|
1787
|
+
if data is None or is_nan(data):
|
|
1788
|
+
return None
|
|
1789
|
+
return str(data)
|
|
1790
|
+
|
|
1791
|
+
|
|
1753
1792
|
@commands.cast_backend_to_python.register(Context, UUID, Backend, object)
|
|
1754
1793
|
def cast_backend_to_python(context: Context, dtype: UUID, backend: Backend, data: Any, **kwargs) -> Any:
|
|
1755
1794
|
if is_nan(data):
|
|
@@ -1891,6 +1930,13 @@ def cast_backend_to_python(context: Context, dtype: Ref, backend: Backend, data:
|
|
|
1891
1930
|
|
|
1892
1931
|
processed_data = {}
|
|
1893
1932
|
for key in data:
|
|
1933
|
+
if key == "_id":
|
|
1934
|
+
# _id reaches this dispatch already in its final form — produced by
|
|
1935
|
+
# handle_ref_key_assignment for external readers, or read directly
|
|
1936
|
+
# from the storage column for internal backends. Re-applying the
|
|
1937
|
+
# referenced model's _id cast double-encodes Base32 ids.
|
|
1938
|
+
processed_data[key] = data[key]
|
|
1939
|
+
continue
|
|
1894
1940
|
prop = commands.resolve_property(dtype.prop.model, f"{dtype.prop.place}.{key}")
|
|
1895
1941
|
if prop is not None:
|
|
1896
1942
|
processed_data[key] = commands.cast_backend_to_python(context, prop, backend, data[key], **kwargs)
|
|
@@ -1939,6 +1985,18 @@ def cast_backend_to_python(context: Context, dtype: Denorm, backend: Backend, da
|
|
|
1939
1985
|
return commands.cast_backend_to_python(context, dtype.rel_prop, backend, data, **kwargs)
|
|
1940
1986
|
|
|
1941
1987
|
|
|
1988
|
+
@commands.cast_backend_to_python.register(Context, Base32, Backend, object)
|
|
1989
|
+
def cast_backend_to_python(context: Context, dtype: Base32, backend: Backend, data: Any, **kwargs) -> Any:
|
|
1990
|
+
if is_nan(data):
|
|
1991
|
+
return None
|
|
1992
|
+
if isinstance(data, (list, tuple)):
|
|
1993
|
+
data = cbor_dumps(list(data))
|
|
1994
|
+
else:
|
|
1995
|
+
data = str(data).encode("utf-8")
|
|
1996
|
+
encoded = base64.b32encode(data)
|
|
1997
|
+
return encoded.rstrip(b"=").decode("utf-8")
|
|
1998
|
+
|
|
1999
|
+
|
|
1942
2000
|
@commands.reload_backend_metadata.register(Context, Manifest, Backend)
|
|
1943
2001
|
def reload_backend_metadata(context, manifest, backend):
|
|
1944
2002
|
pass
|
spinta/backends/helpers.py
CHANGED
|
@@ -26,7 +26,7 @@ from spinta.components import Model
|
|
|
26
26
|
from spinta.components import Namespace
|
|
27
27
|
from spinta.components import Property
|
|
28
28
|
from spinta.exceptions import BackendUnavailable
|
|
29
|
-
from spinta.types.datatype import DataType, Denorm
|
|
29
|
+
from spinta.types.datatype import DataType, Denorm, String, Base32, PrimaryKey
|
|
30
30
|
from spinta.utils.data import take
|
|
31
31
|
from spinta.backends.constants import TableType, BackendOrigin
|
|
32
32
|
|
|
@@ -596,3 +596,28 @@ def extract_table_data_from_logical_name(table_name: str) -> tuple[str | None, T
|
|
|
596
596
|
return data[0], table_type, None
|
|
597
597
|
|
|
598
598
|
return None, None, None
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def is_custom_id_prop(prop: Property) -> bool:
|
|
602
|
+
return prop.name == "_id" and not isinstance(prop.dtype, PrimaryKey)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def is_custom_revision_prop(prop: Property) -> bool:
|
|
606
|
+
return prop.name == "_revision" and prop.explicitly_given
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def is_accessible_by_equals_sign(id_prop: Property, value: str | int) -> bool:
|
|
610
|
+
if isinstance(id_prop.dtype, Base32):
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
if isinstance(id_prop.dtype, String):
|
|
614
|
+
return not check_if_model_primary_key_is_composite(id_prop.model)
|
|
615
|
+
|
|
616
|
+
return False
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def check_if_model_primary_key_is_composite(model: Model) -> bool:
|
|
620
|
+
pkeys_count = len(model.external.pkeys)
|
|
621
|
+
if pkeys_count > 1:
|
|
622
|
+
return True
|
|
623
|
+
return False
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
)
|
spinta/cli/comment.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typer import Argument
|
|
2
|
+
from typer import Context as TyperContext
|
|
3
|
+
from typer import Option
|
|
4
|
+
from typer import echo
|
|
5
|
+
|
|
6
|
+
from spinta.cli.helpers.enums import CommentPart
|
|
7
|
+
from spinta.cli.helpers.store import load_manifest
|
|
8
|
+
from spinta.components import Context, Property
|
|
9
|
+
from spinta.core.context import configure_context
|
|
10
|
+
from spinta.core.enums import Access
|
|
11
|
+
from spinta.manifests.components import Manifest
|
|
12
|
+
from spinta.manifests.tabular.helpers import datasets_to_tabular
|
|
13
|
+
from spinta.manifests.tabular.helpers import render_tabular_manifest_rows
|
|
14
|
+
from spinta.manifests.tabular.helpers import write_tabular_manifest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def comment(
|
|
18
|
+
ctx: TyperContext,
|
|
19
|
+
part: CommentPart = Argument(None, help="Part to comment"),
|
|
20
|
+
author: str | None = Option(None, "--author", help="Comment author"),
|
|
21
|
+
uri: str | None = Option(
|
|
22
|
+
None, "--uri", help="URI tag stored on each comment (use with uncomment --uri to restore selectively)"
|
|
23
|
+
),
|
|
24
|
+
description: str | None = Option(None, "--description", help="Additional description stored on each comment"),
|
|
25
|
+
output: str | None = Option(None, "-o", "--output", help="Output tabular manifest in a specified file"),
|
|
26
|
+
manifests: list[str] = Argument(None, help="Source manifest files"),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Comment out specific manifest parts so the manifest can be processed further."""
|
|
29
|
+
context: Context = ctx.obj
|
|
30
|
+
context = configure_context(context, manifests)
|
|
31
|
+
|
|
32
|
+
comment_manifest(
|
|
33
|
+
context,
|
|
34
|
+
author=author,
|
|
35
|
+
uri=uri,
|
|
36
|
+
description=description,
|
|
37
|
+
output=output,
|
|
38
|
+
manifests=manifests,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def comment_manifest(
|
|
43
|
+
context: Context,
|
|
44
|
+
author: str | None = None,
|
|
45
|
+
uri: str | None = None,
|
|
46
|
+
description: str | None = None,
|
|
47
|
+
output: str | None = None,
|
|
48
|
+
manifests: list[str] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
verbose = bool(output)
|
|
51
|
+
context = configure_context(context, manifests)
|
|
52
|
+
store = load_manifest(
|
|
53
|
+
context,
|
|
54
|
+
load_internal=False,
|
|
55
|
+
verbose=verbose,
|
|
56
|
+
full_load=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_update_systemic_comments(context, store.manifest, author=author, uri=uri, description=description)
|
|
60
|
+
rows = datasets_to_tabular(context, store.manifest, external=False, access=Access.private)
|
|
61
|
+
|
|
62
|
+
if output:
|
|
63
|
+
write_tabular_manifest(context, output, rows)
|
|
64
|
+
else:
|
|
65
|
+
manager = context.get("error_manager")
|
|
66
|
+
handler = manager.handler
|
|
67
|
+
table = render_tabular_manifest_rows(rows)
|
|
68
|
+
if handler.get_counts():
|
|
69
|
+
handler.post_process()
|
|
70
|
+
else:
|
|
71
|
+
echo(table)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _update_systemic_comments(
|
|
75
|
+
context: Context,
|
|
76
|
+
manifest: Manifest,
|
|
77
|
+
*,
|
|
78
|
+
author: str | None = None,
|
|
79
|
+
uri: str | None = None,
|
|
80
|
+
description: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Update author/uri/description on comment type properties."""
|
|
83
|
+
from spinta import commands
|
|
84
|
+
|
|
85
|
+
if all(argument is None for argument in [author, uri, description]):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
models = commands.get_models(context, manifest)
|
|
89
|
+
for model in models.values():
|
|
90
|
+
for prop in model.properties.values():
|
|
91
|
+
_patch_property_restore_comments(prop, author=author, uri=uri, description=description)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _patch_property_restore_comments(
|
|
95
|
+
prop: Property,
|
|
96
|
+
*,
|
|
97
|
+
author: str | None,
|
|
98
|
+
uri: str | None,
|
|
99
|
+
description: str | None,
|
|
100
|
+
) -> None:
|
|
101
|
+
if not prop.comments:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
for comment in prop.comments:
|
|
105
|
+
if not comment.prepare or not comment.prepare.startswith("update"):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if author is not None:
|
|
109
|
+
comment.author = author
|
|
110
|
+
if uri is not None:
|
|
111
|
+
comment.uri = uri
|
|
112
|
+
if description is not None:
|
|
113
|
+
comment.comment = description
|
|
@@ -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
|
+
)
|
spinta/cli/helpers/message.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
f"Warning: {
|
|
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
|
-
|
|
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
|
|
spinta/cli/main.py
CHANGED
|
@@ -17,6 +17,8 @@ from spinta.cli import data
|
|
|
17
17
|
from spinta.cli import inspect
|
|
18
18
|
from spinta.cli import manifest
|
|
19
19
|
from spinta.cli import migrate
|
|
20
|
+
from spinta.cli.comment import comment
|
|
21
|
+
from spinta.cli.uncomment import uncomment
|
|
20
22
|
from spinta.cli import pii
|
|
21
23
|
from spinta.cli import pull
|
|
22
24
|
from spinta.cli import push
|
|
@@ -45,6 +47,8 @@ add(app, "inspect", inspect.inspect, short_help=("Update manifest schema from an
|
|
|
45
47
|
add(app, "pii", pii.app, short_help="Manage Person Identifying Information")
|
|
46
48
|
|
|
47
49
|
add(app, "copy", manifest.copy, short_help="Copy only specified metadata from a manifest")
|
|
50
|
+
add(app, "comment", comment, short_help="Comment unsupported functionality in a manifest")
|
|
51
|
+
add(app, "uncomment", uncomment, short_help="Restore commented parts of a manifest")
|
|
48
52
|
add(app, "show", show, short_help="Show manifest as ascii table")
|
|
49
53
|
|
|
50
54
|
add(app, "bootstrap", migrate.bootstrap, short_help="Initialize backends")
|
spinta/cli/manifest.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Iterator, Union
|
|
2
2
|
from typing import List
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
4
|
from typer import Argument
|
|
6
5
|
from typer import Context as TyperContext
|
|
@@ -36,9 +35,10 @@ def copy(
|
|
|
36
35
|
# https://github.com/tiangolo/typer/issues/151
|
|
37
36
|
access: str = Option("private", help=("Copy properties with at least specified access")),
|
|
38
37
|
format_names: bool = Option(False, help=("Reformat model and property names.")),
|
|
39
|
-
output:
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
output: str | None = Option(None, "-o", "--output", help=("Output tabular manifest in a specified file")),
|
|
39
|
+
dataset: str | None = Option(None, "-d", "--dataset", help=("Main dataset name")),
|
|
40
|
+
columns: str | None = Option(None, "-c", "--columns", help=("Comma separated list of columns")),
|
|
41
|
+
order_by: str | None = Option(
|
|
42
42
|
None, help=("Order by a specified column (currently only access column is supported)")
|
|
43
43
|
),
|
|
44
44
|
rename_duplicates: bool = Option(False, help=("Rename duplicate model names by adding number suffix")),
|
|
@@ -57,6 +57,7 @@ def copy(
|
|
|
57
57
|
order_by=order_by,
|
|
58
58
|
rename_duplicates=rename_duplicates,
|
|
59
59
|
manifests=manifests,
|
|
60
|
+
dataset=dataset,
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
|
|
@@ -65,12 +66,13 @@ def copy_manifest(
|
|
|
65
66
|
source: bool = True,
|
|
66
67
|
access: str = "private",
|
|
67
68
|
format_names: bool = False,
|
|
68
|
-
output:
|
|
69
|
-
columns:
|
|
70
|
-
order_by:
|
|
69
|
+
output: str | None = None,
|
|
70
|
+
columns: str | None = None,
|
|
71
|
+
order_by: str | None = None,
|
|
71
72
|
rename_duplicates: bool = False,
|
|
72
73
|
manifests: List[str] = None,
|
|
73
|
-
output_type:
|
|
74
|
+
output_type: str | None = None,
|
|
75
|
+
dataset: str | None = None,
|
|
74
76
|
):
|
|
75
77
|
"""Copy models from CSV manifest files into another CSV manifest file"""
|
|
76
78
|
access = get_enum_by_name(Access, access)
|
|
@@ -113,7 +115,7 @@ def copy_manifest(
|
|
|
113
115
|
)
|
|
114
116
|
if output:
|
|
115
117
|
if output_type == "mermaid":
|
|
116
|
-
write_mermaid_manifest(context,
|
|
118
|
+
write_mermaid_manifest(context, rows, dataset, output)
|
|
117
119
|
elif internal:
|
|
118
120
|
write_internal_sql_manifest(context, output, rows)
|
|
119
121
|
else:
|