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.
Files changed (58) hide show
  1. spinta/backends/__init__.py +60 -2
  2. spinta/backends/helpers.py +26 -1
  3. spinta/cli/admin.py +12 -8
  4. spinta/cli/comment.py +113 -0
  5. spinta/cli/helpers/admin/components.py +1 -0
  6. spinta/cli/helpers/admin/registry.py +4 -0
  7. spinta/cli/helpers/admin/scripts/enums.py +148 -0
  8. spinta/cli/helpers/enums.py +5 -0
  9. spinta/cli/helpers/message.py +6 -2
  10. spinta/cli/helpers/script/components.py +1 -1
  11. spinta/cli/helpers/script/core.py +5 -7
  12. spinta/cli/helpers/script/helpers.py +2 -2
  13. spinta/cli/main.py +4 -0
  14. spinta/cli/manifest.py +11 -9
  15. spinta/cli/uncomment.py +110 -0
  16. spinta/components.py +12 -0
  17. spinta/config.py +1 -0
  18. spinta/core/access.py +2 -0
  19. spinta/datasets/backends/dataframe/backends/soap/commands/read.py +13 -1
  20. spinta/datasets/backends/dataframe/commands/read.py +5 -2
  21. spinta/datasets/backends/dataframe/ufuncs/query/ufuncs.py +11 -1
  22. spinta/datasets/backends/helpers.py +2 -1
  23. spinta/datasets/backends/sql/commands/cast.py +20 -17
  24. spinta/datasets/backends/sql/commands/read.py +50 -15
  25. spinta/datasets/backends/sql/ufuncs/query/ufuncs.py +11 -1
  26. spinta/datasets/components.py +0 -1
  27. spinta/datasets/helpers.py +36 -3
  28. spinta/dimensions/comments/components.py +3 -0
  29. spinta/dimensions/comments/helpers.py +2 -0
  30. spinta/dimensions/scope/__init__.py +0 -0
  31. spinta/dimensions/scope/components.py +46 -0
  32. spinta/dimensions/scope/helpers.py +39 -0
  33. spinta/exceptions.py +24 -5
  34. spinta/formats/html/commands.py +8 -5
  35. spinta/formats/html/helpers.py +7 -1
  36. spinta/manifests/internal_sql/helpers.py +4 -2
  37. spinta/manifests/mermaid/helpers.py +251 -180
  38. spinta/manifests/tabular/components.py +19 -0
  39. spinta/manifests/tabular/helpers.py +110 -6
  40. spinta/testing/csv.py +7 -2
  41. spinta/types/__init__.py +1 -0
  42. spinta/types/array/__init__.py +1 -0
  43. spinta/types/array/link.py +6 -3
  44. spinta/types/backref/__init__.py +1 -0
  45. spinta/types/backref/link.py +9 -3
  46. spinta/types/datatype.py +11 -0
  47. spinta/types/helpers.py +67 -2
  48. spinta/types/model.py +44 -2
  49. spinta/types/ref/__init__.py +1 -0
  50. spinta/types/ref/link.py +5 -3
  51. spinta/urlparams.py +2 -0
  52. spinta/utils/naming.py +2 -2
  53. spinta/utils/url.py +3 -0
  54. {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/METADATA +2 -1
  55. {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/RECORD +58 -51
  56. {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/WHEEL +0 -0
  57. {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/entry_points.txt +0 -0
  58. {spinta-0.2.dev22.dist-info → spinta-0.2.dev24.dist-info}/licenses/LICENSE +0 -0
@@ -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 Array, ExternalRef, Inherit, PageType, BackRef, ArrayBackRef, Integer, Boolean, Denorm
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
@@ -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=("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
  )
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
@@ -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
+ )
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CommentPart(str, Enum):
5
+ missing_external_refs = "missing-external-refs"
@@ -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
 
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: Optional[str] = Option(None, "-o", "--output", help=("Output tabular manifest in a specified file")),
40
- columns: Optional[str] = Option(None, "-c", "--columns", help=("Comma separated list of columns")),
41
- order_by: Optional[str] = Option(
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: Optional[str] = None,
69
- columns: Optional[str] = None,
70
- order_by: Optional[str] = None,
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: Optional[str] = None,
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, output, rows)
118
+ write_mermaid_manifest(context, rows, dataset, output)
117
119
  elif internal:
118
120
  write_internal_sql_manifest(context, output, rows)
119
121
  else: