elastro-client 1.3.53__tar.gz → 1.3.55__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 (155) hide show
  1. {elastro_client-1.3.53/elastro_client.egg-info → elastro_client-1.3.55}/PKG-INFO +1 -1
  2. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/__init__.py +10 -1
  3. elastro_client-1.3.55/elastro/cli/commands/ingest.py +569 -0
  4. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/document.py +104 -0
  5. elastro_client-1.3.55/elastro/core/document_bulk.py +45 -0
  6. elastro_client-1.3.55/elastro/core/ingest/__init__.py +31 -0
  7. elastro_client-1.3.55/elastro/core/ingest/engine.py +314 -0
  8. elastro_client-1.3.55/elastro/core/ingest/readers.py +289 -0
  9. elastro_client-1.3.55/elastro/core/ingest/validators.py +403 -0
  10. elastro_client-1.3.55/elastro/server/__init__.py +216 -0
  11. elastro_client-1.3.55/elastro/server/routes/__init__.py +15 -0
  12. elastro_client-1.3.55/elastro/server/routes/cli.py +112 -0
  13. elastro_client-1.3.55/elastro/server/routes/clusters.py +177 -0
  14. elastro_client-1.3.55/elastro/server/routes/config.py +46 -0
  15. elastro_client-1.3.55/elastro/server/routes/indices.py +178 -0
  16. elastro_client-1.3.55/elastro/server/schemas.py +27 -0
  17. elastro_client-1.3.55/elastro/server/services.py +87 -0
  18. {elastro_client-1.3.53 → elastro_client-1.3.55/elastro_client.egg-info}/PKG-INFO +1 -1
  19. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/SOURCES.txt +16 -1
  20. {elastro_client-1.3.53 → elastro_client-1.3.55}/pyproject.toml +2 -3
  21. elastro_client-1.3.55/tests/unit/core/test_document_bulk.py +197 -0
  22. elastro_client-1.3.55/tests/unit/core/test_ingest_engine.py +205 -0
  23. elastro_client-1.3.55/tests/unit/core/test_ingest_integration.py +347 -0
  24. elastro_client-1.3.55/tests/unit/core/test_ingest_readers.py +177 -0
  25. elastro_client-1.3.55/tests/unit/core/test_ingest_validators.py +199 -0
  26. elastro_client-1.3.53/elastro/cli/commands/ingest.py +0 -92
  27. elastro_client-1.3.53/elastro/core/document_bulk.py +0 -117
  28. elastro_client-1.3.53/elastro/server.py +0 -777
  29. elastro_client-1.3.53/tests/unit/core/test_document_bulk.py +0 -162
  30. {elastro_client-1.3.53 → elastro_client-1.3.55}/.coveragerc +0 -0
  31. {elastro_client-1.3.53 → elastro_client-1.3.55}/LICENSE +0 -0
  32. {elastro_client-1.3.53 → elastro_client-1.3.55}/MANIFEST.in +0 -0
  33. {elastro_client-1.3.53 → elastro_client-1.3.55}/README.md +0 -0
  34. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/advanced_features.md +0 -0
  35. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/api_reference.md +0 -0
  36. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/cli_usage.md +0 -0
  37. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/commands_reference.md +0 -0
  38. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/getting_started.md +0 -0
  39. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/roadmap.md +0 -0
  40. {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/troubleshooting.md +0 -0
  41. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/__init__.py +0 -0
  42. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/aggregations.py +0 -0
  43. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/query_builder.py +0 -0
  44. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/scroll.py +0 -0
  45. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/__init__.py +0 -0
  46. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/art.py +0 -0
  47. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/cli.py +0 -0
  48. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/__init__.py +0 -0
  49. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/cluster.py +0 -0
  50. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/config.py +0 -0
  51. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/daemon.py +0 -0
  52. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/datastream.py +0 -0
  53. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/document.py +0 -0
  54. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/gui.py +0 -0
  55. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/ilm.py +0 -0
  56. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/index.py +0 -0
  57. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/index_recipes.py +0 -0
  58. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/memory.py +0 -0
  59. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/ml.py +0 -0
  60. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/painless_commands.py +0 -0
  61. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/rag.py +0 -0
  62. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/script.py +0 -0
  63. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/security.py +0 -0
  64. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/snapshot.py +0 -0
  65. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/tasks.py +0 -0
  66. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/telemetry.py +0 -0
  67. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/template.py +0 -0
  68. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/tools.py +0 -0
  69. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/utils.py +0 -0
  70. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/completion.py +0 -0
  71. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/output.py +0 -0
  72. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/__init__.py +0 -0
  73. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/defaults.py +0 -0
  74. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/loader.py +0 -0
  75. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/__init__.py +0 -0
  76. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/base.py +0 -0
  77. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/client.py +0 -0
  78. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/daemon.py +0 -0
  79. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/datastream.py +0 -0
  80. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/errors.py +0 -0
  81. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/ilm.py +0 -0
  82. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/index.py +0 -0
  83. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/logger.py +0 -0
  84. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/middleware.py +0 -0
  85. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/query_builder.py +0 -0
  86. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/rag/ast_parser.py +0 -0
  87. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/rag/ingestor.py +0 -0
  88. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/snapshot.py +0 -0
  89. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/validation.py +0 -0
  90. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/assets/index-CeUjjtn-.css +0 -0
  91. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/assets/index-DNdGuJvV.js +0 -0
  92. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/elastro.svg +0 -0
  93. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/favicon.ico +0 -0
  94. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/index.html +0 -0
  95. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/vite.svg +0 -0
  96. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/py.typed +0 -0
  97. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/__init__.py +0 -0
  98. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/aliases.py +0 -0
  99. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/health.py +0 -0
  100. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/snapshots.py +0 -0
  101. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/templates.py +0 -0
  102. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/dependency_links.txt +0 -0
  103. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/entry_points.txt +0 -0
  104. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/requires.txt +0 -0
  105. {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/top_level.txt +0 -0
  106. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/client.py +0 -0
  107. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/config_usage.py +0 -0
  108. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/datastreams.py +0 -0
  109. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/debug_connection.py +0 -0
  110. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/document_operations.py +0 -0
  111. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/index_management.py +0 -0
  112. {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/search.py +0 -0
  113. {elastro_client-1.3.53 → elastro_client-1.3.55}/pytest.ini +0 -0
  114. {elastro_client-1.3.53 → elastro_client-1.3.55}/requirements.txt +0 -0
  115. {elastro_client-1.3.53 → elastro_client-1.3.55}/setup.cfg +0 -0
  116. {elastro_client-1.3.53 → elastro_client-1.3.55}/setup.py +0 -0
  117. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/__init__.py +0 -0
  118. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/conftest.py +0 -0
  119. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/__init__.py +0 -0
  120. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/datastream_fixtures.py +0 -0
  121. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/document_fixtures.py +0 -0
  122. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/index_fixtures.py +0 -0
  123. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/__init__.py +0 -0
  124. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_aggregations_integration.py +0 -0
  125. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_client_integration.py +0 -0
  126. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_datastream_integration.py +0 -0
  127. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_docs_quickstart.py +0 -0
  128. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_document_integration.py +0 -0
  129. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_index_integration.py +0 -0
  130. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_query_builder_integration.py +0 -0
  131. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_scroll_integration.py +0 -0
  132. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_workflow_integration.py +0 -0
  133. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/manual/test_es.py +0 -0
  134. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/__init__.py +0 -0
  135. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/__init__.py +0 -0
  136. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_aggregations.py +0 -0
  137. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_query_builder.py +0 -0
  138. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_scroll.py +0 -0
  139. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/__init__.py +0 -0
  140. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/test_defaults.py +0 -0
  141. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/test_loader.py +0 -0
  142. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/__init__.py +0 -0
  143. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_client.py +0 -0
  144. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_datastream.py +0 -0
  145. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_document.py +0 -0
  146. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_errors.py +0 -0
  147. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_index.py +0 -0
  148. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_validation.py +0 -0
  149. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/__init__.py +0 -0
  150. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_aliases.py +0 -0
  151. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_health.py +0 -0
  152. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_snapshots.py +0 -0
  153. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_templates.py +0 -0
  154. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/verify_api_actions.py +0 -0
  155. {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/verify_cli_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastro-client
3
- Version: 1.3.53
3
+ Version: 1.3.55
4
4
  Summary: A comprehensive Python library for Elasticsearch management with both programmatic and CLI interfaces
5
5
  Author: Austin Jorgensen
6
6
  License-Expression: MIT
@@ -4,17 +4,26 @@ Elasticsearch Management Module.
4
4
  A module for managing Elasticsearch operations within a pipeline process.
5
5
  """
6
6
 
7
- __version__ = "1.3.53"
7
+ from importlib.metadata import version, PackageNotFoundError
8
+
9
+ try:
10
+ __version__ = version("elastro-client")
11
+ except PackageNotFoundError:
12
+ # Fallback for editable installs or when metadata is unavailable
13
+ __version__ = "1.3.55"
8
14
 
9
15
  # Core component imports
10
16
  from elastro.core.client import ElasticsearchClient
11
17
  from elastro.core.index import IndexManager
12
18
  from elastro.core.document import DocumentManager
13
19
  from elastro.core.datastream import DatastreamManager
20
+ from elastro.core.ingest.engine import IngestEngine, IngestResult
14
21
 
15
22
  __all__ = [
16
23
  "ElasticsearchClient",
17
24
  "IndexManager",
18
25
  "DocumentManager",
19
26
  "DatastreamManager",
27
+ "IngestEngine",
28
+ "IngestResult",
20
29
  ]
@@ -0,0 +1,569 @@
1
+ """
2
+ Ingest CLI commands — pipeline management + data import/profile/validate.
3
+
4
+ Extends the existing pipeline listing and simulation commands with
5
+ Phase 1 capabilities: multi-format import, data profiling, auto-mapping
6
+ inference, and schema validation.
7
+ """
8
+
9
+ import json
10
+ import rich_click as click
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
18
+
19
+ from elastro.core.client import ElasticsearchClient
20
+
21
+
22
+ @click.group(name="ingest")
23
+ def ingest_group() -> None:
24
+ """
25
+ Data ingest engine and pipeline management.
26
+
27
+ Import data from CSV, NDJSON, JSON, or SQL sources into Elasticsearch
28
+ with optional schema validation, type coercion, and PII detection.
29
+ """
30
+ pass
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Existing pipeline commands (preserved)
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @ingest_group.command(name="pipelines")
39
+ @click.option("--id", help="Fetch a specific pipeline ID")
40
+ @click.pass_obj
41
+ def list_pipelines(client: ElasticsearchClient, id: str) -> None:
42
+ """
43
+ List or fetch ingest pipelines.
44
+ """
45
+ console = Console()
46
+ try:
47
+ es = client.client
48
+ res = es.ingest.get_pipeline(id=id) if id else es.ingest.get_pipeline()
49
+
50
+ table = Table(show_header=True, header_style="bold magenta")
51
+ table.add_column("Pipeline ID")
52
+ table.add_column("Description")
53
+ table.add_column("Version")
54
+ table.add_column("Processor Count")
55
+
56
+ for k, v in res.items():
57
+ table.add_row(
58
+ k,
59
+ str(v.get("description", "N/A")),
60
+ str(v.get("version", "N/A")),
61
+ str(len(v.get("processors", []))),
62
+ )
63
+
64
+ console.print(table)
65
+ except Exception as e:
66
+ console.print(f"[bold red]Ingest Error:[/bold red] {str(e)}")
67
+
68
+
69
+ @ingest_group.command(name="simulate")
70
+ @click.argument("id")
71
+ @click.option(
72
+ "--doc",
73
+ "-d",
74
+ help="Raw JSON Document String to simulate against the ingest nodes",
75
+ )
76
+ @click.option(
77
+ "--file",
78
+ "-f",
79
+ type=click.File("r"),
80
+ help="Path to a JSON document file to simulate",
81
+ )
82
+ @click.pass_obj
83
+ def simulate_pipeline(
84
+ client: ElasticsearchClient,
85
+ id: str,
86
+ doc: Optional[str],
87
+ file: Any,
88
+ ) -> None:
89
+ """
90
+ Simulate a document through an ingest pipeline.
91
+
92
+ Provide the document via --doc (JSON string) or --file (JSON file).
93
+
94
+ Examples:
95
+
96
+ Simulate with inline JSON:
97
+ ```bash
98
+ elastro ingest simulate my-pipeline --doc '{"message": "hello world"}'
99
+ ```
100
+
101
+ Simulate from file:
102
+ ```bash
103
+ elastro ingest simulate my-pipeline --file ./sample.json
104
+ ```
105
+ """
106
+ console = Console()
107
+
108
+ if not doc and not file:
109
+ console.print("[bold red]Error:[/bold red] Provide either --doc or --file")
110
+ raise SystemExit(1)
111
+
112
+ try:
113
+ es = client.client
114
+
115
+ if file:
116
+ doc_json = json.load(file)
117
+ else:
118
+ doc_json = json.loads(doc) # type: ignore[arg-type]
119
+
120
+ if "_source" not in doc_json:
121
+ doc_json = {"_source": doc_json}
122
+
123
+ body = {"docs": [doc_json]}
124
+ res = es.ingest.simulate(id=id, body=body)
125
+
126
+ console.print(f"[bold green]Simulation Results for Pipeline: {id}[/bold green]")
127
+ for doc_res in res.get("docs", []):
128
+ if "error" in doc_res:
129
+ console.print("[bold red]ERROR![/bold red]")
130
+ console.print(doc_res["error"])
131
+ else:
132
+ console.print(doc_res.get("doc", {}))
133
+
134
+ except json.JSONDecodeError:
135
+ console.print(
136
+ "[bold red]Simulation Error:[/bold red] The document must be valid JSON."
137
+ )
138
+ except Exception as e:
139
+ console.print(f"[bold red]Simulation Error:[/bold red] {str(e)}")
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Phase 1: Import
144
+ # ---------------------------------------------------------------------------
145
+
146
+
147
+ @ingest_group.command(name="import", no_args_is_help=True)
148
+ @click.argument("source", type=str)
149
+ @click.option("--index", "-i", required=True, help="Target Elasticsearch index")
150
+ @click.option(
151
+ "--format",
152
+ "-f",
153
+ "fmt",
154
+ type=click.Choice(["auto", "csv", "ndjson", "json"]),
155
+ default="auto",
156
+ help="Source file format (auto-detects from extension)",
157
+ )
158
+ @click.option("--delimiter", help="CSV delimiter override (default: ',')")
159
+ @click.option("--encoding", default="utf-8", help="File encoding")
160
+ @click.option("--batch-size", type=int, default=2000, help="Documents per bulk request")
161
+ @click.option("--max-errors", type=int, default=100, help="Abort after N errors")
162
+ @click.option("--pipeline", help="ES ingest pipeline to apply server-side")
163
+ @click.option(
164
+ "--validate/--no-validate", default=False, help="Enable schema validation"
165
+ )
166
+ @click.option("--strict", is_flag=True, help="Strict mode: reject on type mismatch")
167
+ @click.option("--dlq", type=click.Path(), help="Dead-letter queue output file")
168
+ @click.option("--refresh", is_flag=True, help="Refresh index after each batch")
169
+ @click.pass_obj
170
+ def import_data(
171
+ client: ElasticsearchClient,
172
+ source: str,
173
+ index: str,
174
+ fmt: str,
175
+ delimiter: Optional[str],
176
+ encoding: str,
177
+ batch_size: int,
178
+ max_errors: int,
179
+ pipeline: Optional[str],
180
+ validate: bool,
181
+ strict: bool,
182
+ dlq: Optional[str],
183
+ refresh: bool,
184
+ ) -> None:
185
+ """
186
+ Import data from CSV, NDJSON, or JSON into Elasticsearch.
187
+
188
+ Supports streaming ingestion with progress reporting, optional schema
189
+ validation, type coercion, and dead-letter queue for failed documents.
190
+
191
+ Examples:
192
+
193
+ Import a CSV file:
194
+ ```bash
195
+ elastro ingest import customers.csv --index customers
196
+ ```
197
+
198
+ Import NDJSON with validation:
199
+ ```bash
200
+ elastro ingest import events.ndjson --index events --validate
201
+ ```
202
+
203
+ Import with DLQ and pipeline:
204
+ ```bash
205
+ elastro ingest import data.json --index logs --pipeline my-pipeline --dlq ./failed.ndjson
206
+ ```
207
+
208
+ Import from stdin:
209
+ ```bash
210
+ cat data.csv | elastro ingest import - --index logs --format csv
211
+ ```
212
+ """
213
+ from elastro.core.ingest.engine import IngestEngine
214
+
215
+ console = Console()
216
+ engine = IngestEngine(client)
217
+
218
+ console.print(
219
+ Panel.fit(
220
+ f"[bold cyan]Elastro Ingest Engine[/bold cyan]\n"
221
+ f"Source: [green]{source}[/green]\n"
222
+ f"Target: [green]{index}[/green]\n"
223
+ f"Format: {fmt} | Batch: {batch_size} | Validate: {validate}",
224
+ border_style="cyan",
225
+ )
226
+ )
227
+
228
+ with Progress(
229
+ SpinnerColumn(),
230
+ TextColumn("[progress.description]{task.description}"),
231
+ BarColumn(),
232
+ TextColumn("[bold blue]{task.completed} docs"),
233
+ console=console,
234
+ ) as progress:
235
+ task = progress.add_task("Ingesting...", total=None)
236
+
237
+ result = engine.ingest(
238
+ source,
239
+ index,
240
+ format=fmt,
241
+ delimiter=delimiter,
242
+ encoding=encoding,
243
+ batch_size=batch_size,
244
+ max_errors=max_errors,
245
+ pipeline=pipeline,
246
+ validate=validate,
247
+ strict=strict,
248
+ dlq_path=dlq,
249
+ refresh=refresh,
250
+ )
251
+
252
+ progress.update(task, completed=result.total_read)
253
+
254
+ # Display results
255
+ status = (
256
+ "[bold green]SUCCESS[/bold green]"
257
+ if result.total_failed == 0
258
+ else "[bold yellow]COMPLETED WITH ERRORS[/bold yellow]"
259
+ )
260
+ console.print(f"\n{status}")
261
+
262
+ results_table = Table(show_header=False, box=None)
263
+ results_table.add_column("Metric", style="dim")
264
+ results_table.add_column("Value", style="bold")
265
+
266
+ results_table.add_row("Documents Read", str(result.total_read))
267
+ results_table.add_row("Documents Indexed", f"[green]{result.total_indexed}[/green]")
268
+ results_table.add_row(
269
+ "Documents Failed",
270
+ f"[red]{result.total_failed}[/red]" if result.total_failed else "0",
271
+ )
272
+ results_table.add_row("Success Rate", f"{result.success_rate:.1f}%")
273
+ results_table.add_row("Elapsed", f"{result.elapsed_seconds:.2f}s")
274
+
275
+ if result.total_read > 0:
276
+ rate = result.total_read / max(result.elapsed_seconds, 0.001)
277
+ results_table.add_row("Throughput", f"{rate:,.0f} docs/sec")
278
+
279
+ if result.dlq_path:
280
+ results_table.add_row("Dead-Letter Queue", result.dlq_path)
281
+
282
+ console.print(results_table)
283
+
284
+ if result.total_failed > 0:
285
+ raise SystemExit(1)
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # Phase 1: Profile
290
+ # ---------------------------------------------------------------------------
291
+
292
+
293
+ @ingest_group.command(name="profile", no_args_is_help=True)
294
+ @click.argument("source", type=str)
295
+ @click.option(
296
+ "--format",
297
+ "-f",
298
+ "fmt",
299
+ type=click.Choice(["auto", "csv", "ndjson", "json"]),
300
+ default="auto",
301
+ )
302
+ @click.option("--sample-size", type=int, default=1000, help="Number of rows to sample")
303
+ @click.option("--delimiter", help="CSV delimiter override")
304
+ @click.pass_obj
305
+ def profile_data_cmd(
306
+ client: ElasticsearchClient,
307
+ source: str,
308
+ fmt: str,
309
+ sample_size: int,
310
+ delimiter: Optional[str],
311
+ ) -> None:
312
+ """
313
+ Profile a data source before importing.
314
+
315
+ Analyzes field types, null rates, uniqueness, and PII risk
316
+ without sending any data to Elasticsearch.
317
+
318
+ Examples:
319
+
320
+ Profile a CSV file:
321
+ ```bash
322
+ elastro ingest profile customers.csv
323
+ ```
324
+
325
+ Profile with larger sample:
326
+ ```bash
327
+ elastro ingest profile events.ndjson --sample-size 5000
328
+ ```
329
+ """
330
+ from elastro.core.ingest.readers import read_source
331
+ from elastro.core.ingest.validators import profile_data
332
+
333
+ console = Console()
334
+
335
+ console.print(
336
+ f"[bold cyan]Profiling[/bold cyan] {source} (sample: {sample_size} rows)\n"
337
+ )
338
+
339
+ docs = read_source(source, format=fmt, delimiter=delimiter)
340
+ report = profile_data(docs, sample_size=sample_size)
341
+
342
+ # Render table
343
+ table = Table(title=f"Data Profile ({report['total_rows_sampled']} rows sampled)")
344
+ table.add_column("Field", style="bold cyan")
345
+ table.add_column("Type", style="green")
346
+ table.add_column("Non-Null %", justify="right")
347
+ table.add_column("Unique %", justify="right")
348
+ table.add_column("PII Risk", justify="center")
349
+ table.add_column("Sample Values", style="dim")
350
+
351
+ for f in report["fields"]:
352
+ pii_style = {
353
+ "NONE": "[green]✅ NONE[/green]",
354
+ "HIGH": "[yellow]⚠️ HIGH[/yellow]",
355
+ "PII": "[red]🔴 PII[/red]",
356
+ }.get(f["pii_risk"], f["pii_risk"])
357
+
358
+ samples = ", ".join(f["sample_values"][:3])
359
+ if len(samples) > 50:
360
+ samples = samples[:47] + "..."
361
+
362
+ table.add_row(
363
+ f["field"],
364
+ f["inferred_type"],
365
+ f"{f['non_null_pct']}%",
366
+ f"{f['unique_pct']:.1f}%",
367
+ pii_style,
368
+ samples,
369
+ )
370
+
371
+ console.print(table)
372
+
373
+ pii_count = report["pii_risk_fields"]
374
+ if pii_count:
375
+ console.print(
376
+ f"\n[bold yellow]⚠ {pii_count} field(s) flagged for PII risk.[/bold yellow] "
377
+ "Consider using --sanitize during import."
378
+ )
379
+ console.print(
380
+ f"\n[dim]{report['total_fields']} fields, {report['total_rows_sampled']} rows sampled[/dim]"
381
+ )
382
+
383
+
384
+ # ---------------------------------------------------------------------------
385
+ # Phase 1: Auto-Map
386
+ # ---------------------------------------------------------------------------
387
+
388
+
389
+ @ingest_group.command(name="auto-map", no_args_is_help=True)
390
+ @click.argument("source", type=str)
391
+ @click.option(
392
+ "--format",
393
+ "-f",
394
+ "fmt",
395
+ type=click.Choice(["auto", "csv", "ndjson", "json"]),
396
+ default="auto",
397
+ )
398
+ @click.option("--sample-size", type=int, default=500, help="Documents to sample")
399
+ @click.option("--delimiter", help="CSV delimiter override")
400
+ @click.option("--output", "-o", type=click.Path(), help="Write mapping JSON to file")
401
+ @click.pass_obj
402
+ def auto_map(
403
+ client: ElasticsearchClient,
404
+ source: str,
405
+ fmt: str,
406
+ sample_size: int,
407
+ delimiter: Optional[str],
408
+ output: Optional[str],
409
+ ) -> None:
410
+ """
411
+ Infer an Elasticsearch mapping from a data source.
412
+
413
+ Samples the source file and uses heuristics to determine optimal
414
+ field types. Outputs a ready-to-use ES mapping JSON.
415
+
416
+ Examples:
417
+
418
+ Auto-map a CSV:
419
+ ```bash
420
+ elastro ingest auto-map customers.csv
421
+ ```
422
+
423
+ Save mapping to file:
424
+ ```bash
425
+ elastro ingest auto-map events.ndjson --output mapping.json
426
+ ```
427
+ """
428
+ from elastro.core.ingest.readers import read_source
429
+ from elastro.core.ingest.validators import infer_mapping
430
+
431
+ console = Console()
432
+
433
+ console.print(
434
+ f"[bold cyan]Inferring mapping[/bold cyan] from {source} (sample: {sample_size})\n"
435
+ )
436
+
437
+ docs = read_source(source, format=fmt, delimiter=delimiter)
438
+ mapping = infer_mapping(docs, sample_size=sample_size)
439
+
440
+ json_str = json.dumps(mapping, indent=2)
441
+
442
+ if output:
443
+ Path(output).write_text(json_str, encoding="utf-8")
444
+ console.print(f"[bold green]✓[/bold green] Mapping written to {output}")
445
+ else:
446
+ console.print(Syntax(json_str, "json", theme="monokai"))
447
+
448
+ props = mapping.get("mappings", {}).get("properties", {})
449
+ console.print(f"\n[dim]{len(props)} fields inferred[/dim]")
450
+
451
+
452
+ # ---------------------------------------------------------------------------
453
+ # Phase 1: Validate (dry-run)
454
+ # ---------------------------------------------------------------------------
455
+
456
+
457
+ @ingest_group.command(name="validate", no_args_is_help=True)
458
+ @click.argument("source", type=str)
459
+ @click.option("--index", "-i", required=True, help="Target index to validate against")
460
+ @click.option(
461
+ "--format",
462
+ "-f",
463
+ "fmt",
464
+ type=click.Choice(["auto", "csv", "ndjson", "json"]),
465
+ default="auto",
466
+ )
467
+ @click.option("--sample-size", type=int, default=500, help="Documents to validate")
468
+ @click.option("--strict", is_flag=True, help="Strict mode (no type coercion)")
469
+ @click.option("--delimiter", help="CSV delimiter override")
470
+ @click.pass_obj
471
+ def validate_data(
472
+ client: ElasticsearchClient,
473
+ source: str,
474
+ index: str,
475
+ fmt: str,
476
+ sample_size: int,
477
+ strict: bool,
478
+ delimiter: Optional[str],
479
+ ) -> None:
480
+ """
481
+ Validate data against an index mapping (dry-run).
482
+
483
+ Fetches the mapping from the target index, validates sample documents,
484
+ and reports any type mismatches or missing fields — without indexing.
485
+
486
+ Examples:
487
+
488
+ Validate CSV against existing index:
489
+ ```bash
490
+ elastro ingest validate customers.csv --index customers
491
+ ```
492
+
493
+ Strict validation (no coercion):
494
+ ```bash
495
+ elastro ingest validate data.json --index logs --strict
496
+ ```
497
+ """
498
+ from elastro.core.ingest.readers import read_source
499
+ from elastro.core.ingest.validators import SchemaValidator
500
+
501
+ console = Console()
502
+
503
+ # Fetch mapping from index
504
+ try:
505
+ es = client.client
506
+ idx_info = es.indices.get(index=index)
507
+ if hasattr(idx_info, "body"):
508
+ idx_info = idx_info.body
509
+ idx_data = idx_info.get(index, {})
510
+ props = idx_data.get("mappings", {}).get("properties", {})
511
+
512
+ if not props:
513
+ console.print(
514
+ f"[yellow]No mapping found for index '{index}'. Skipping validation.[/yellow]"
515
+ )
516
+ return
517
+
518
+ console.print(
519
+ f"[bold cyan]Validating[/bold cyan] {source} against [green]{index}[/green] ({len(props)} mapped fields)\n"
520
+ )
521
+
522
+ except Exception as e:
523
+ console.print(f"[bold red]Error fetching mapping:[/bold red] {e}")
524
+ raise SystemExit(1)
525
+
526
+ validator = SchemaValidator(props, strict=strict)
527
+ docs = read_source(source, format=fmt, delimiter=delimiter)
528
+
529
+ total = 0
530
+ valid = 0
531
+ invalid = 0
532
+ all_errors: list[dict[str, Any]] = []
533
+
534
+ for doc in docs:
535
+ if total >= sample_size:
536
+ break
537
+ total += 1
538
+ is_valid, _, errors = validator.validate(doc)
539
+ if is_valid:
540
+ valid += 1
541
+ else:
542
+ invalid += 1
543
+ if len(all_errors) < 20:
544
+ all_errors.append({"row": total, "errors": errors})
545
+
546
+ # Results
547
+ if invalid == 0:
548
+ console.print(f"[bold green]✓ All {total} documents valid[/bold green]")
549
+ else:
550
+ console.print(
551
+ f"[bold yellow]⚠ {invalid}/{total} documents have validation errors[/bold yellow]\n"
552
+ )
553
+
554
+ err_table = Table(title="Validation Errors (first 20)")
555
+ err_table.add_column("Row", style="dim", justify="right")
556
+ err_table.add_column("Errors", style="red")
557
+
558
+ for entry in all_errors:
559
+ err_table.add_row(
560
+ str(entry["row"]),
561
+ "; ".join(entry["errors"]),
562
+ )
563
+
564
+ console.print(err_table)
565
+
566
+ mode = "strict" if strict else "coerce"
567
+ console.print(
568
+ f"\n[dim]Mode: {mode} | {total} docs checked | {valid} valid, {invalid} invalid[/dim]"
569
+ )