validio-cli 0.29.0__tar.gz → 5.3.0__tar.gz

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 (27) hide show
  1. {validio_cli-0.29.0 → validio_cli-5.3.0}/PKG-INFO +1 -1
  2. {validio_cli-0.29.0 → validio_cli-5.3.0}/pyproject.toml +1 -1
  3. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/__init__.py +6 -3
  4. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/sources.py +128 -221
  5. {validio_cli-0.29.0 → validio_cli-5.3.0}/LICENSE +0 -0
  6. {validio_cli-0.29.0 → validio_cli-5.3.0}/README_PUBLIC.md +0 -0
  7. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/channels.py +0 -0
  8. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/code.py +0 -0
  9. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/config.py +0 -0
  10. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/credentials.py +0 -0
  11. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/dbt.py +0 -0
  12. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/filters.py +0 -0
  13. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/incidents.py +0 -0
  14. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/metrics.py +0 -0
  15. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/namespaces.py +0 -0
  16. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/notification_rules.py +0 -0
  17. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/resources.py +0 -0
  18. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/segmentations.py +0 -0
  19. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/segments.py +0 -0
  20. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/users.py +0 -0
  21. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/validators.py +0 -0
  22. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/entities/windows.py +0 -0
  23. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/bin/main.py +0 -0
  24. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/components.py +0 -0
  25. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/metadata.py +0 -0
  26. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/namespace.py +0 -0
  27. {validio_cli-0.29.0 → validio_cli-5.3.0}/validio_cli/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: validio-cli
3
- Version: 0.29.0
3
+ Version: 5.3.0
4
4
  Summary: CLI tool to interact with the Validio platform
5
5
  Home-page: https://validio.io/
6
6
  License: Apache-2.0
@@ -3,7 +3,7 @@ name = "validio-cli"
3
3
  # This version does not represent the released version or any tag. For each
4
4
  # release we automatically bump this before building and publishing so this
5
5
  # should be kept at 0.0.1dev1
6
- version = "0.29.0"
6
+ version = "5.3.0"
7
7
  description = "CLI tool to interact with the Validio platform"
8
8
  authors = ["Validio <support@validio.io>"]
9
9
  license = "Apache-2.0"
@@ -138,7 +138,7 @@ def output_json(obj: Any, identifier: str | None = None) -> None:
138
138
  print(j)
139
139
 
140
140
 
141
- def output_text(items: Any, fields: dict[str, OutputSettings | None]) -> None:
141
+ def format_text(items: Any, fields: dict[str, OutputSettings | None]) -> str:
142
142
  if items is None:
143
143
  items = []
144
144
  elif not isinstance(items, list):
@@ -175,8 +175,11 @@ def output_text(items: Any, fields: dict[str, OutputSettings | None]) -> None:
175
175
 
176
176
  table.append(row)
177
177
 
178
- datatable = tabulate(table, tablefmt="plain")
179
- print(datatable)
178
+ return tabulate(table, tablefmt="plain")
179
+
180
+
181
+ def output_text(items: Any, fields: dict[str, OutputSettings | None]) -> None:
182
+ print(format_text(items=items, fields=fields))
180
183
 
181
184
  # We flush stdout here so we can avoid an unhandled broken pipe error.
182
185
  # Catching the exception according to the documentation at
@@ -2,7 +2,7 @@ import asyncio
2
2
  import json
3
3
  import uuid
4
4
  from pathlib import Path
5
- from typing import Any, cast
5
+ from typing import Any, Optional, cast
6
6
 
7
7
  import click
8
8
  import typer
@@ -30,12 +30,14 @@ from validio_cli import (
30
30
  OutputFormatOption,
31
31
  OutputSettings,
32
32
  components,
33
+ format_text,
33
34
  get_client,
34
35
  output_json,
35
36
  output_ok_or_error,
36
37
  output_text,
37
38
  )
38
39
  from validio_cli.bin.entities import credentials
40
+ from validio_cli.components import proceed_with_operation
39
41
  from validio_cli.namespace import get_namespace
40
42
 
41
43
  app = AsyncTyper(help="Data sources to ingest data from")
@@ -132,25 +134,96 @@ async def delete(
132
134
  return output_ok_or_error(result)
133
135
 
134
136
 
137
+ async def prompt_for_start_all_sources(
138
+ client: APIClient,
139
+ cfg: ValidioConfig,
140
+ namespace: str,
141
+ auto_approve: bool,
142
+ ) -> list[str] | None:
143
+ if not namespace:
144
+ raise typer.BadParameter("Namespace must be supplied when using --all")
145
+
146
+ sources = await client.get_sources(
147
+ namespace_id=get_namespace(namespace, cfg),
148
+ )
149
+
150
+ if not isinstance(sources, list):
151
+ raise ValidioError("failed to get sources")
152
+
153
+ if len(sources) == 0:
154
+ print("No sources exist in namespace to start")
155
+ return None
156
+
157
+ sources_to_start = [
158
+ source for source in sources if source.get("state") in ["IDLE", "INIT"]
159
+ ]
160
+
161
+ if len(sources_to_start) == 0:
162
+ print("No sources in namespace that need starting")
163
+ return None
164
+
165
+ prompt = "The following sources will be started:\n"
166
+ prompt += format_text(
167
+ sources_to_start,
168
+ fields={
169
+ "id": OutputSettings(attribute_name="id"),
170
+ "resource_name": OutputSettings(attribute_name="resourceName"),
171
+ "type": OutputSettings.trimmed_upper_snake(
172
+ attribute_name="__typename", trim="Source"
173
+ ),
174
+ "state": None,
175
+ },
176
+ )
177
+
178
+ print(prompt)
179
+
180
+ if not await proceed_with_operation(auto_approve):
181
+ return None
182
+ print()
183
+
184
+ return [source["id"] for source in sources_to_start]
185
+
186
+
135
187
  @app.async_command(help="Start sources")
136
188
  async def start(
137
189
  config_dir: str = ConfigDir,
138
190
  output_format: OutputFormat = OutputFormatOption,
139
191
  # ruff: noqa: ARG001
140
192
  namespace: str = Namespace(),
141
- identifiers: list[str] = Identifiers,
193
+ all: bool = typer.Option(False, "--all", help="Start all unstarted sources"),
194
+ auto_approve: bool = typer.Option(
195
+ False,
196
+ help="When used with --all, automatically approve starting of all sources",
197
+ ),
198
+ identifiers: Optional[list[str]] = Identifiers,
142
199
  ) -> None:
143
200
  client, cfg = get_client(config_dir)
144
- async with client.client as session:
145
- await apply_source_action_on_chunks(
146
- session,
147
- cfg,
148
- SourceAction.START,
149
- split_to_chunks(identifiers),
150
- namespace,
151
- output_format,
201
+
202
+ if all and identifiers and len(identifiers) > 0:
203
+ raise typer.BadParameter(
204
+ "Cannot use both --all and supplying source "
205
+ "identifiers. You must only use one of them"
152
206
  )
153
207
 
208
+ if all:
209
+ identifiers = await prompt_for_start_all_sources(
210
+ client=client,
211
+ cfg=cfg,
212
+ namespace=namespace,
213
+ auto_approve=auto_approve,
214
+ )
215
+
216
+ if identifiers:
217
+ async with client.client as session:
218
+ await apply_source_action_on_chunks(
219
+ session,
220
+ cfg,
221
+ SourceAction.START,
222
+ split_to_chunks(identifiers),
223
+ namespace,
224
+ output_format,
225
+ )
226
+
154
227
 
155
228
  @app.async_command(help="Stop source")
156
229
  async def stop(
@@ -271,13 +344,11 @@ async def _interactive(config_dir: str, filename: Path, namespace: str) -> None:
271
344
  credential_type_to_source_type = {
272
345
  "AwsCredential": [
273
346
  ("kinesis", "Amazon Kinesis"),
274
- ("s3", "Amazon S3"),
275
347
  ],
276
348
  "DatabricksCredential": [("databricks", "Databricks")],
277
349
  "DemoCredential": [("demo", "Demo")],
278
350
  "GcpCredential": [
279
351
  ("bigquery", "Google BigQuery"),
280
- ("gcs", "Google Cloud Storage"),
281
352
  ("pubsub", "Google Pub/Sub"),
282
353
  ],
283
354
  "PostgreSqlCredential": [("postgresql", "PostgreSQL")],
@@ -458,29 +529,6 @@ async def _interactive(config_dir: str, filename: Path, namespace: str) -> None:
458
529
  info.get("message_format", ""),
459
530
  info.get("message_schema", ""),
460
531
  )
461
- case "s3":
462
- info = await _multip_prompt(
463
- [
464
- ("Bucket", [], ""),
465
- ("File pattern", [], "*.csv"),
466
- ("Prefix", [], ""),
467
- ("CSV delimiter", [",", "|"], ","),
468
- ]
469
- )
470
-
471
- print()
472
- null_marker = await _get_null_marker()
473
-
474
- await _infer_schema_s3(
475
- vc,
476
- filename,
477
- credential_id,
478
- info.get("bucket", ""),
479
- info.get("file_pattern", ""),
480
- info.get("prefix", ""),
481
- info.get("csv_delimiter", ""),
482
- null_marker,
483
- )
484
532
  case "postgresql":
485
533
  info = await _multip_prompt(
486
534
  [
@@ -549,31 +597,6 @@ async def _interactive(config_dir: str, filename: Path, namespace: str) -> None:
549
597
  info.get("dataset", ""),
550
598
  info.get("table", ""),
551
599
  )
552
- case "gcs":
553
- info = await _multip_prompt(
554
- [
555
- ("Project", [], ""),
556
- ("Bucket", [], ""),
557
- ("Folder", [], ""),
558
- ("File pattern", ["*.csv"], "*.csv"),
559
- ("CSV delimiter", [",", "|"], ","),
560
- ]
561
- )
562
-
563
- print()
564
- null_marker = await _get_null_marker()
565
-
566
- await _infer_schema_gcs(
567
- vc,
568
- filename,
569
- credential_id,
570
- info.get("project", ""),
571
- info.get("bucket", ""),
572
- info.get("folder", ""),
573
- info.get("file_pattern", ""),
574
- info.get("csv_delimiter", ""),
575
- null_marker,
576
- )
577
600
  case _:
578
601
  print("Not yet implemented...")
579
602
  return
@@ -588,40 +611,6 @@ async def demo(
588
611
  await _infer_schema_demo(vc, filename)
589
612
 
590
613
 
591
- @infer_schema_app.async_command(help="Infer Amazon S3 schema")
592
- async def s3(
593
- config_dir: str = ConfigDir,
594
- filename: Path = schema_filename_option("s3"),
595
- namespace: str = Namespace(),
596
- credential_id: str = typer.Option(..., help="Credential name or ID"),
597
- bucket: str = typer.Option(..., help="S3 bucket name"),
598
- file_pattern: str = typer.Option(
599
- "*.csv", help="File glob pattern - files to use for inference"
600
- ),
601
- prefix: str = typer.Option("", help="Prefix in the bucket, a directory"),
602
- csv_delimiter: str = typer.Option(",", help="Delimiter between columns in file"),
603
- null_marker: str = typer.Option(None, help="Null marker (values to treat as NULL)"),
604
- ) -> None:
605
- vc, cfg = get_client(config_dir)
606
-
607
- resolved_credential_id = await credentials.get_credential_id(
608
- vc, cfg, credential_id, namespace
609
- )
610
- if resolved_credential_id is None:
611
- return
612
-
613
- await _infer_schema_s3(
614
- vc,
615
- filename,
616
- resolved_credential_id,
617
- bucket,
618
- file_pattern,
619
- prefix,
620
- csv_delimiter,
621
- null_marker,
622
- )
623
-
624
-
625
614
  @infer_schema_app.async_command(help="Infer Amazon Kinesis schema")
626
615
  async def kinesis(
627
616
  config_dir: str = ConfigDir,
@@ -792,40 +781,6 @@ async def bigquery(
792
781
  )
793
782
 
794
783
 
795
- @infer_schema_app.async_command(help="Infer Google Cloud Storage schema")
796
- async def gcs(
797
- config_dir: str = ConfigDir,
798
- filename: Path = schema_filename_option("gcs"),
799
- namespace: str = Namespace(),
800
- credential_id: str = typer.Option(..., help="Credential name or ID"),
801
- project: str = typer.Option(..., help="Google project name"),
802
- bucket: str = typer.Option(..., help="GCS bucket"),
803
- folder: str = typer.Option("", help="Folder in the bucket"),
804
- file_pattern: str = typer.Option("*.csv", help="File glob"),
805
- csv_delimiter: str = typer.Option(",", help="Delimiter between columns in file"),
806
- null_marker: str = typer.Option(None, help="Null marker (values to treat as NULL)"),
807
- ) -> None:
808
- vc, cfg = get_client(config_dir)
809
-
810
- resolved_credential_id = await credentials.get_credential_id(
811
- vc, cfg, credential_id, namespace
812
- )
813
- if resolved_credential_id is None:
814
- return
815
-
816
- await _infer_schema_gcs(
817
- vc,
818
- filename,
819
- resolved_credential_id,
820
- project,
821
- bucket,
822
- folder,
823
- file_pattern,
824
- csv_delimiter,
825
- null_marker,
826
- )
827
-
828
-
829
784
  async def _infer_schema_databricks(
830
785
  vc: APIClient,
831
786
  filename: Path,
@@ -855,32 +810,6 @@ async def _infer_schema_demo(
855
810
  _write_schema(filename, schema)
856
811
 
857
812
 
858
- async def _infer_schema_s3(
859
- vc: APIClient,
860
- filename: Path,
861
- credential_id: str,
862
- bucket: str,
863
- file_pattern: str,
864
- prefix: str,
865
- csv_delimiter: str,
866
- null_marker: str | None,
867
- ) -> None:
868
- schema = await vc.infer_schema(
869
- class_name="AwsS3",
870
- variable_values={
871
- "credentialId": credential_id,
872
- "bucket": bucket,
873
- "filePattern": file_pattern,
874
- "prefix": prefix,
875
- "csv": {
876
- "delimiter": csv_delimiter,
877
- "nullMarker": null_marker,
878
- },
879
- },
880
- )
881
- _write_schema(filename, schema)
882
-
883
-
884
813
  async def _infer_schema_kinesis(
885
814
  vc: APIClient,
886
815
  filename: Path,
@@ -1029,34 +958,6 @@ async def _infer_schema_bigquery(
1029
958
  _write_schema(filename, schema)
1030
959
 
1031
960
 
1032
- async def _infer_schema_gcs(
1033
- vc: APIClient,
1034
- filename: Path,
1035
- credential_id: str,
1036
- project: str,
1037
- bucket: str,
1038
- folder: str,
1039
- file_pattern: str,
1040
- csv_delimiter: str,
1041
- null_marker: str | None,
1042
- ) -> None:
1043
- schema = await vc.infer_schema(
1044
- class_name="GcpStorage",
1045
- variable_values={
1046
- "credentialId": credential_id,
1047
- "project": project,
1048
- "bucket": bucket,
1049
- "folder": folder,
1050
- "filePattern": file_pattern,
1051
- "csv": {
1052
- "delimiter": csv_delimiter,
1053
- "nullMarker": null_marker,
1054
- },
1055
- },
1056
- )
1057
- _write_schema(filename, schema)
1058
-
1059
-
1060
961
  def _write_schema(filename: Path, schema: Any) -> None:
1061
962
  filename.write_text(json.dumps(schema, indent=2))
1062
963
 
@@ -1081,38 +982,6 @@ async def _multip_prompt(questions: list[tuple[str, list[str], str]]) -> dict[st
1081
982
  return answers
1082
983
 
1083
984
 
1084
- async def _get_null_marker() -> str | None:
1085
- null_marker = await components.radiolist_dialog(
1086
- title="Null marker - What value should be treated as NULL",
1087
- values=[
1088
- ("none", "None (No value is treated as NULL)"),
1089
- ("NULL", "The literal string 'NULL', f.ex. 'a,NULL,c'"),
1090
- ("empty", "Empty (\"\", f.ex 'a,,c')"),
1091
- ("other", "Set manually"),
1092
- ],
1093
- )
1094
-
1095
- # User hit escape sequence, don't return None in this case.
1096
- if null_marker is None:
1097
- raise typer.Exit(code=1)
1098
-
1099
- if null_marker == "other":
1100
- session: PromptSession = PromptSession()
1101
- null_marker = await session.prompt_async(
1102
- validio_cli._fixed_width("Null marker"),
1103
- )
1104
-
1105
- if null_marker == "none":
1106
- return None
1107
-
1108
- # We can't map the value to '""' because it will make it pre-selected in the
1109
- # dialog which we don't want.
1110
- if null_marker == "empty":
1111
- return ""
1112
-
1113
- return null_marker
1114
-
1115
-
1116
985
  async def _get_source_id(
1117
986
  session: Session,
1118
987
  cfg: ValidioConfig,
@@ -1227,7 +1096,11 @@ async def apply_source_action_on_chunks(
1227
1096
  ]
1228
1097
  )
1229
1098
 
1230
- return handle_source_action_response(results, action, output_format)
1099
+ return await handle_source_action_response(
1100
+ results,
1101
+ action,
1102
+ output_format,
1103
+ )
1231
1104
 
1232
1105
 
1233
1106
  async def apply_batch_source_action(
@@ -1253,15 +1126,17 @@ async def apply_single_source_action(
1253
1126
  source_id = await _get_source_id(session, cfg, identifier, namespace)
1254
1127
  if source_id is None:
1255
1128
  return {
1256
- f"source{action.value}": {
1257
- "errors": [{"message": f"No source with id or name {identifier} found"}]
1258
- }
1129
+ "errors": [{"message": f"No source with id or name {identifier} found"}]
1259
1130
  }
1260
1131
 
1261
- return await apply_source_action(session, action, source_id)
1132
+ result = await apply_source_action(session, action, source_id)
1133
+ return {
1134
+ "id": source_id,
1135
+ **result,
1136
+ }
1262
1137
 
1263
1138
 
1264
- def handle_source_action_response(
1139
+ async def handle_source_action_response(
1265
1140
  results: list[list[dict[str, Any]]],
1266
1141
  action: SourceAction,
1267
1142
  output_format: OutputFormat,
@@ -1290,7 +1165,19 @@ def handle_source_action_response(
1290
1165
  if output_format == OutputFormat.JSON:
1291
1166
  return output_json(result)
1292
1167
 
1293
- return output_text(
1168
+ output_action_word = ""
1169
+ match action:
1170
+ case SourceAction.START:
1171
+ output_action_word = "starting"
1172
+ case SourceAction.STOP:
1173
+ output_action_word = "stopping"
1174
+ case SourceAction.BACKFILL:
1175
+ output_action_word = "initiating backfilling"
1176
+ case SourceAction.RESET:
1177
+ output_action_word = "resetting"
1178
+
1179
+ print(f"Summary of results when {output_action_word} sources:")
1180
+ output_text(
1294
1181
  result,
1295
1182
  fields={
1296
1183
  field_name: None,
@@ -1298,6 +1185,26 @@ def handle_source_action_response(
1298
1185
  },
1299
1186
  )
1300
1187
 
1188
+ if len(error_responses) > 0:
1189
+ print()
1190
+ print(f"Failures details when {output_action_word} sources:")
1191
+ output_text(
1192
+ error_responses,
1193
+ fields={
1194
+ "source": OutputSettings(
1195
+ attribute_name="id",
1196
+ ),
1197
+ "error": OutputSettings(
1198
+ attribute_name="errors",
1199
+ reformat=lambda errors: ",".join(
1200
+ [error["message"] for error in errors]
1201
+ ),
1202
+ ),
1203
+ },
1204
+ )
1205
+
1206
+ return None
1207
+
1301
1208
 
1302
1209
  if __name__ == "__main__":
1303
1210
  typer.run(app())
File without changes