table-validator 0.1.18__tar.gz → 0.1.20__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 (57) hide show
  1. {table_validator-0.1.18/table_validator.egg-info → table_validator-0.1.20}/PKG-INFO +5 -1
  2. {table_validator-0.1.18 → table_validator-0.1.20}/README.md +4 -0
  3. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/cli/main.py +22 -2
  4. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/notebook.py +144 -5
  5. {table_validator-0.1.18 → table_validator-0.1.20/table_validator.egg-info}/PKG-INFO +5 -1
  6. table_validator-0.1.20/table_validator.egg-info/scm_version.json +8 -0
  7. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_cli.py +5 -1
  8. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_notebook.py +73 -0
  9. table_validator-0.1.18/table_validator.egg-info/scm_version.json +0 -8
  10. {table_validator-0.1.18 → table_validator-0.1.20}/LICENSE +0 -0
  11. {table_validator-0.1.18 → table_validator-0.1.20}/pyproject.toml +0 -0
  12. {table_validator-0.1.18 → table_validator-0.1.20}/setup.cfg +0 -0
  13. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/__init__.py +0 -0
  14. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/auth/__init__.py +0 -0
  15. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/auth/azure_auth.py +0 -0
  16. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/auth/databricks_auth.py +0 -0
  17. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/cli/__init__.py +0 -0
  18. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/cli/partition_prompt.py +0 -0
  19. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/cli/summary_table.py +0 -0
  20. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/cli/wizard.py +0 -0
  21. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/config/__init__.py +0 -0
  22. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/config/manager.py +0 -0
  23. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/config/schema.py +0 -0
  24. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/connectors/__init__.py +0 -0
  25. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/connectors/azure_connector.py +0 -0
  26. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/connectors/base_sql_connector.py +0 -0
  27. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/connectors/databricks_connector.py +0 -0
  28. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/connectors/spark_connector.py +0 -0
  29. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/engine/__init__.py +0 -0
  30. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/engine/comparison_engine.py +0 -0
  31. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/models.py +0 -0
  32. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/reports/__init__.py +0 -0
  33. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/reports/excel_report.py +0 -0
  34. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/validators/__init__.py +0 -0
  35. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/validators/blob_discovery.py +0 -0
  36. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/validators/catalog_validator.py +0 -0
  37. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/validators/mismatch_classifier.py +0 -0
  38. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator/validators/row_validator.py +0 -0
  39. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/SOURCES.txt +0 -0
  40. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/dependency_links.txt +0 -0
  41. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/entry_points.txt +0 -0
  42. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/requires.txt +0 -0
  43. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/scm_file_list.json +0 -0
  44. {table_validator-0.1.18 → table_validator-0.1.20}/table_validator.egg-info/top_level.txt +0 -0
  45. {table_validator-0.1.18 → table_validator-0.1.20}/tests/__init__.py +0 -0
  46. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_base_sql_connector.py +0 -0
  47. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_blob_discovery.py +0 -0
  48. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_catalog_validator.py +0 -0
  49. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_databricks_connector.py +0 -0
  50. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_excel_report.py +0 -0
  51. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_mismatch_classifier.py +0 -0
  52. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_package_import_without_pyspark.py +0 -0
  53. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_partition_prompt.py +0 -0
  54. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_report_command.py +0 -0
  55. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_row_validator.py +0 -0
  56. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_spark_connector.py +0 -0
  57. {table_validator-0.1.18 → table_validator-0.1.20}/tests/test_wizard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table-validator
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: CLI tool for validating data migrations between Azure (Blob Storage / SQL Database) and Databricks Delta Lake catalogs
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -135,6 +135,10 @@ Overall status: FAIL
135
135
  Tables: 1 total, 0 passed, 1 failed, 0 error, 0 skipped
136
136
  ```
137
137
 
138
+ Call `validate_tables.help()` any time (no arguments, no live Spark
139
+ connection needed) to print a full syntax reference for every keyword
140
+ argument shown below, directly in the notebook cell.
141
+
138
142
  `result` also exposes each of the Excel report's own sheets as a
139
143
  plain-text table - print only the one you want:
140
144
 
@@ -105,6 +105,10 @@ Overall status: FAIL
105
105
  Tables: 1 total, 0 passed, 1 failed, 0 error, 0 skipped
106
106
  ```
107
107
 
108
+ Call `validate_tables.help()` any time (no arguments, no live Spark
109
+ connection needed) to print a full syntax reference for every keyword
110
+ argument shown below, directly in the notebook cell.
111
+
108
112
  `result` also exposes each of the Excel report's own sheets as a
109
113
  plain-text table - print only the one you want:
110
114
 
@@ -124,14 +124,34 @@ def info() -> None:
124
124
  " - Databricks catalog -> Databricks catalog\n"
125
125
  " - Azure Blob Storage -> Databricks catalog\n"
126
126
  " - Azure SQL Database -> Databricks catalog\n"
127
+ " - Azure Synapse SQL pool -> Databricks catalog\n"
127
128
  "\n"
128
129
  "Typical workflow, in order:\n"
129
130
  "\n"
130
131
  " 1. tablevalidator configure\n"
131
132
  " Interactively set up credentials and which source/target "
132
133
  "table(s) to compare. Run this first, and again any time you "
133
- "need to change settings (source type, tables, primary key, "
134
- "which validations to run).\n"
134
+ "need to change settings.\n"
135
+ " When a single source and target table are both named (not a "
136
+ "schema-wide sweep), the wizard also offers these optional steps "
137
+ "- answer 'no'/leave blank to skip any of them and keep today's "
138
+ "plain full-table comparison:\n"
139
+ " - Primary key: a real key column for row-level comparison "
140
+ "instead of the ROW_NUMBER() fallback.\n"
141
+ " - Customize column validation: compare ONLY specific "
142
+ "columns, SKIP specific columns entirely, or ignore datatype "
143
+ "mismatches on specific columns.\n"
144
+ " - Filter to a subset of rows (Databricks source only): "
145
+ "restrict the whole comparison to rows matching a condition, e.g. "
146
+ "\"id > 10 and id < 30\" or \"gender = 'male'\" - either the same "
147
+ "condition on both sides, or a different one per side.\n"
148
+ " - Map renamed columns (Databricks source only): pair up "
149
+ "individual columns that don't share a name between source and "
150
+ "target.\n"
151
+ " When a schema is named on both sides but no specific table "
152
+ "is (a schema-wide sweep, Databricks source only), the wizard "
153
+ "instead offers to restrict the sweep to specific table names, or "
154
+ "map renamed tables (source name -> target name).\n"
135
155
  "\n"
136
156
  " 2. tablevalidator validate\n"
137
157
  " Runs the comparison using the saved configuration and "
@@ -40,11 +40,12 @@ Typical usage, from a Databricks notebook cell:
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
43
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
44
44
 
45
45
  import pandas as pd
46
46
 
47
47
  from table_validator.cli.summary_table import SummaryData, summary_from_response
48
+ from table_validator.config.schema import ValidationType
48
49
  from table_validator.connectors.spark_connector import SparkConnector
49
50
  from table_validator.models import CatalogValidationRequest, CatalogValidationResponse
50
51
  from table_validator.reports.excel_report import (
@@ -285,6 +286,7 @@ def validate_tables(
285
286
  row_filter: Optional[str] = None,
286
287
  source_row_filter: Optional[str] = None,
287
288
  target_row_filter: Optional[str] = None,
289
+ enabled_validations: Optional[Set[ValidationType]] = None,
288
290
  ) -> ValidationResult:
289
291
  """
290
292
  Validate a source against a target, using the notebook's own ambient
@@ -355,6 +357,15 @@ def validate_tables(
355
357
  filtering every matched table by the same condition, e.g.
356
358
  `gender = 'male'`, is a legitimate use case).
357
359
 
360
+ `enabled_validations`, if given, restricts WHICH of the four check
361
+ groups run - a set of `ValidationType.CATALOG`/`SCHEMA`/`COLUMN`/`ROW`
362
+ (import from `table_validator.config.schema`). If omitted (the
363
+ default), all four run, identical to today's behavior. Mirrors the
364
+ CLI wizard's own "Which validations should run?" checkbox
365
+ (`ValidatorConfig.validations`) - e.g. pass
366
+ `{ValidationType.SCHEMA}` to check only schema/column shape without
367
+ ever comparing row counts or data.
368
+
358
369
  Returns a ValidationResult: `print(result)` alone shows a compact
359
370
  plain-text summary (no rich/box-drawing formatting, safe to print
360
371
  directly in a notebook cell, and correctly listing every table when
@@ -411,7 +422,7 @@ def validate_tables(
411
422
  "for a schema-wide sweep) - got a table on only one side."
412
423
  )
413
424
 
414
- request = CatalogValidationRequest(
425
+ request_kwargs: Dict[str, Any] = dict(
415
426
  source_catalog=src_catalog,
416
427
  target_catalog=tgt_catalog,
417
428
  schemas=[src_schema],
@@ -423,11 +434,139 @@ def validate_tables(
423
434
  only_columns=only_columns,
424
435
  column_map=column_map or {},
425
436
  ignore_datatype_columns=ignore_datatype_columns or [],
426
- # max_tier and enabled_validations deliberately left unset - the
427
- # Pydantic model defaults already equal the CLI's own "full" mode
428
- # (see this function's docstring above).
437
+ # max_tier deliberately left unset - the Pydantic model default
438
+ # already equals the CLI's own "full" mode (see this function's
439
+ # docstring above).
429
440
  )
441
+ if enabled_validations is not None:
442
+ request_kwargs["enabled_validations"] = enabled_validations
443
+ # else: leave enabled_validations unset - the Pydantic model default
444
+ # (all four ValidationType values) matches today's unchanged behavior.
445
+
446
+ request = CatalogValidationRequest(**request_kwargs)
430
447
 
431
448
  response = CatalogValidator(connector).compare_catalogs(request)
432
449
 
433
450
  return ValidationResult(response)
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # validate_tables.help() - a code/notebook-usage reference, callable
455
+ # directly on the function (`from table_validator import validate_tables;
456
+ # validate_tables.help()`), separate from `tablevalidator info` (the CLI's
457
+ # own usage reference, cli/main.py's info() command) - each covers only the
458
+ # approach it belongs to, since a CLI-only user has no use for keyword-
459
+ # argument syntax and a notebook-only user never touches config.yaml.
460
+ # ---------------------------------------------------------------------------
461
+ _HELP_TEXT = """
462
+ validate_tables() - notebook-native table validation
463
+ ------------------------------------------------------
464
+ Compares a source table against a target Databricks table (or every
465
+ identically-named table across two schemas), from inside a Databricks
466
+ notebook cell - no workspace URL, personal access token, or SQL Warehouse
467
+ HTTP path to set up, since it reuses the notebook's own already-
468
+ authenticated Spark session. Runs the exact same full-depth comparison
469
+ engine as the `tablevalidator` CLI.
470
+
471
+ Install and import:
472
+ %pip install table-validator
473
+ from table_validator import validate_tables
474
+
475
+ Single-table comparison ("catalog.schema.table" on both sides):
476
+ result = validate_tables(
477
+ "catalog1.schema1.table1",
478
+ "catalog1.schema1.table2",
479
+ )
480
+
481
+ Schema-wide sweep (no table named - "catalog.schema" on both sides):
482
+ compares every identically-named table in that schema in one call:
483
+ result = validate_tables("catalog1.bronze", "catalog2.silver")
484
+
485
+ If some tables were renamed between source and target, pass table_map
486
+ (source name -> target name) - unmapped tables are still matched by
487
+ identical name as usual:
488
+ result = validate_tables(
489
+ "catalog1.bronze", "catalog2.silver",
490
+ table_map={"cust": "customers", "ord": "orders"},
491
+ )
492
+
493
+ Keyword arguments (all optional):
494
+ primary_key Real key column(s) for row-level comparison
495
+ instead of the synthetic ROW_NUMBER() fallback.
496
+ Single-table mode only.
497
+ primary_key=["id"]
498
+ table_map Renamed-table pairs, schema-wide sweep mode only.
499
+ table_map={"cust": "customers"}
500
+ spark An explicit SparkSession, if you're not
501
+ running inside an actual Databricks notebook
502
+ (e.g. local development). Auto-detected
503
+ otherwise.
504
+ ignore_columns Columns excluded entirely from every check.
505
+ ignore_columns=["updated_at"]
506
+ only_columns If set, ONLY these columns (plus the primary
507
+ key, if any) are compared.
508
+ only_columns=["id", "name"]
509
+ column_map Renamed-column pairs (source name -> target
510
+ name), for a column that doesn't share a name
511
+ between source and target.
512
+ column_map={"cust_id": "customer_id"}
513
+ ignore_datatype_columns Columns whose data-type mismatch is ignored -
514
+ their other checks (nullable, statistics,
515
+ row-hash) still run normally.
516
+ ignore_datatype_columns=["legacy_flag"]
517
+ row_filter Restrict comparison to rows matching a SQL
518
+ WHERE-fragment, on BOTH sides - row count,
519
+ statistics, fingerprint, and row-hash/column
520
+ diff are all scoped to just the matching rows.
521
+ row_filter="id > 20 and id < 100"
522
+ row_filter="gender = 'male'"
523
+ source_row_filter Like row_filter, but applied only to the
524
+ target_row_filter source/target side respectively - combine
525
+ with row_filter (ANDed) or use instead of it
526
+ when the condition must differ per side.
527
+ validate_tables(
528
+ "cat.sch.orders", "cat2.sch.orders",
529
+ source_row_filter="id > 20",
530
+ target_row_filter="id > 15",
531
+ )
532
+ Works in both single-table and schema-wide
533
+ sweep mode. Each fragment is used as-is, not
534
+ parsed or validated - a typo surfaces as a
535
+ normal SQL error when the query runs.
536
+ enabled_validations Restrict WHICH check groups run - a set of
537
+ ValidationType.CATALOG/SCHEMA/COLUMN/ROW
538
+ (import from table_validator.config.schema).
539
+ Omitted = all four run, same as today.
540
+ from table_validator.config.schema import ValidationType
541
+ validate_tables(
542
+ "cat.sch.orders", "cat2.sch.orders",
543
+ enabled_validations={ValidationType.SCHEMA},
544
+ )
545
+
546
+ Reading the result:
547
+ print(result) Compact plain-text summary.
548
+ print(result.table_validation) One row per table (Excel report's
549
+ "Table Validation" sheet).
550
+ print(result.column_validation) One row per column.
551
+ print(result.data_mismatches) One row per mismatched cell (needs a
552
+ real primary key or the row-number
553
+ fallback, FULL-depth detail only).
554
+ print(result.row_hash_mismatches) One row per mismatched primary key.
555
+ print(result.mismatch_categories) Root-cause breakdown (NULL_MISMATCH,
556
+ STRING_TRUNCATION, ...).
557
+ print(result.suggestions) Plain-English fix suggestions.
558
+
559
+ Each of these is a small table object - .headers/.rows for
560
+ programmatic access, or .to_dataframe() for a real pandas DataFrame.
561
+ result.response is the raw CatalogValidationResponse for full
562
+ programmatic access beyond the sheet breakdown.
563
+
564
+ Call validate_tables.help() any time to print this again.
565
+ """.strip("\n")
566
+
567
+
568
+ def _print_validate_tables_help() -> None:
569
+ print(_HELP_TEXT)
570
+
571
+
572
+ validate_tables.help = _print_validate_tables_help
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table-validator
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: CLI tool for validating data migrations between Azure (Blob Storage / SQL Database) and Databricks Delta Lake catalogs
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -135,6 +135,10 @@ Overall status: FAIL
135
135
  Tables: 1 total, 0 passed, 1 failed, 0 error, 0 skipped
136
136
  ```
137
137
 
138
+ Call `validate_tables.help()` any time (no arguments, no live Spark
139
+ connection needed) to print a full syntax reference for every keyword
140
+ argument shown below, directly in the notebook cell.
141
+
138
142
  `result` also exposes each of the Excel report's own sheets as a
139
143
  plain-text table - print only the one you want:
140
144
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.20",
3
+ "distance": 0,
4
+ "node": "g9124e37c7cb704b705524188c1a7fec5d1848039",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-08"
8
+ }
@@ -50,10 +50,14 @@ def test_info_command_describes_tool_and_workflow_order() -> None:
50
50
  result = runner.invoke(app, ["info"])
51
51
 
52
52
  assert result.exit_code == 0
53
- # All three source types are named.
53
+ # All four source types are named.
54
54
  assert "Databricks catalog" in result.output
55
55
  assert "Azure Blob Storage" in result.output
56
56
  assert "Azure SQL Database" in result.output
57
+ assert "Synapse" in result.output
58
+ # Wizard customization steps are documented, including the row-filter
59
+ # feature.
60
+ assert "Filter to a subset of rows" in result.output
57
61
  # The three main commands appear in workflow order.
58
62
  configure_pos = result.output.index("tablevalidator configure")
59
63
  validate_pos = result.output.index("tablevalidator validate")
@@ -749,3 +749,76 @@ def test_row_filter_valid_in_schema_sweep_mode():
749
749
  fake_connector.set_row_filters.assert_called_once_with(
750
750
  common="gender = 'male'", source=None, target=None,
751
751
  )
752
+
753
+
754
+ # ---------------------------------------------------------------------------
755
+ # validate_tables.help() - a code/notebook-usage reference, separate from
756
+ # the CLI's own `tablevalidator info` command.
757
+ # ---------------------------------------------------------------------------
758
+ def test_validate_tables_help_is_callable_and_prints_usage(capsys):
759
+ """validate_tables.help() must be reachable directly off the imported
760
+ function, print without requiring a SparkSession/Databricks connection,
761
+ and cover the keyword arguments a notebook user needs (row filters in
762
+ particular, the most recently added feature)."""
763
+ assert callable(validate_tables.help)
764
+
765
+ validate_tables.help()
766
+
767
+ captured = capsys.readouterr()
768
+ assert "validate_tables" in captured.out
769
+ assert "row_filter" in captured.out
770
+ assert "primary_key" in captured.out
771
+ assert "table_validation" in captured.out
772
+
773
+
774
+ # ---------------------------------------------------------------------------
775
+ # enabled_validations kwarg - restricts which of the 4 check groups run,
776
+ # needed by tablevalidator-databricks' generated notebook (its checks
777
+ # multiselect maps directly onto this).
778
+ # ---------------------------------------------------------------------------
779
+ def test_enabled_validations_omitted_keeps_default_all_four_types():
780
+ """Regression guard: NOT passing enabled_validations at all must
781
+ produce the exact same request as before this kwarg existed - the
782
+ Pydantic model's own default_factory (all four ValidationType values)
783
+ must still apply, not an empty set."""
784
+ from table_validator.config.schema import ValidationType
785
+
786
+ captured = {}
787
+
788
+ def fake_compare_catalogs(self, request):
789
+ captured["request"] = request
790
+ return _fake_response()
791
+
792
+ with patch("table_validator.notebook.SparkConnector", return_value=MagicMock()), \
793
+ patch(
794
+ "table_validator.notebook.CatalogValidator.compare_catalogs",
795
+ fake_compare_catalogs,
796
+ ):
797
+ validate_tables("cat1.sch1.t1", "cat1.sch1.t2", spark=MagicMock())
798
+
799
+ assert captured["request"].enabled_validations == {
800
+ ValidationType.CATALOG, ValidationType.SCHEMA,
801
+ ValidationType.COLUMN, ValidationType.ROW,
802
+ }
803
+
804
+
805
+ def test_enabled_validations_passed_through_when_given():
806
+ from table_validator.config.schema import ValidationType
807
+
808
+ captured = {}
809
+
810
+ def fake_compare_catalogs(self, request):
811
+ captured["request"] = request
812
+ return _fake_response()
813
+
814
+ with patch("table_validator.notebook.SparkConnector", return_value=MagicMock()), \
815
+ patch(
816
+ "table_validator.notebook.CatalogValidator.compare_catalogs",
817
+ fake_compare_catalogs,
818
+ ):
819
+ validate_tables(
820
+ "cat1.sch1.t1", "cat1.sch1.t2", spark=MagicMock(),
821
+ enabled_validations={ValidationType.SCHEMA},
822
+ )
823
+
824
+ assert captured["request"].enabled_validations == {ValidationType.SCHEMA}
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.1.18",
3
- "distance": 0,
4
- "node": "g442d915637cacc0f99f5a1eb372c1cc7128f7e06",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-03"
8
- }