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.
- {elastro_client-1.3.53/elastro_client.egg-info → elastro_client-1.3.55}/PKG-INFO +1 -1
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/__init__.py +10 -1
- elastro_client-1.3.55/elastro/cli/commands/ingest.py +569 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/document.py +104 -0
- elastro_client-1.3.55/elastro/core/document_bulk.py +45 -0
- elastro_client-1.3.55/elastro/core/ingest/__init__.py +31 -0
- elastro_client-1.3.55/elastro/core/ingest/engine.py +314 -0
- elastro_client-1.3.55/elastro/core/ingest/readers.py +289 -0
- elastro_client-1.3.55/elastro/core/ingest/validators.py +403 -0
- elastro_client-1.3.55/elastro/server/__init__.py +216 -0
- elastro_client-1.3.55/elastro/server/routes/__init__.py +15 -0
- elastro_client-1.3.55/elastro/server/routes/cli.py +112 -0
- elastro_client-1.3.55/elastro/server/routes/clusters.py +177 -0
- elastro_client-1.3.55/elastro/server/routes/config.py +46 -0
- elastro_client-1.3.55/elastro/server/routes/indices.py +178 -0
- elastro_client-1.3.55/elastro/server/schemas.py +27 -0
- elastro_client-1.3.55/elastro/server/services.py +87 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55/elastro_client.egg-info}/PKG-INFO +1 -1
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/SOURCES.txt +16 -1
- {elastro_client-1.3.53 → elastro_client-1.3.55}/pyproject.toml +2 -3
- elastro_client-1.3.55/tests/unit/core/test_document_bulk.py +197 -0
- elastro_client-1.3.55/tests/unit/core/test_ingest_engine.py +205 -0
- elastro_client-1.3.55/tests/unit/core/test_ingest_integration.py +347 -0
- elastro_client-1.3.55/tests/unit/core/test_ingest_readers.py +177 -0
- elastro_client-1.3.55/tests/unit/core/test_ingest_validators.py +199 -0
- elastro_client-1.3.53/elastro/cli/commands/ingest.py +0 -92
- elastro_client-1.3.53/elastro/core/document_bulk.py +0 -117
- elastro_client-1.3.53/elastro/server.py +0 -777
- elastro_client-1.3.53/tests/unit/core/test_document_bulk.py +0 -162
- {elastro_client-1.3.53 → elastro_client-1.3.55}/.coveragerc +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/LICENSE +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/MANIFEST.in +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/README.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/advanced_features.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/api_reference.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/cli_usage.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/commands_reference.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/getting_started.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/roadmap.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/docs/troubleshooting.md +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/aggregations.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/query_builder.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/advanced/scroll.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/art.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/cli.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/cluster.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/config.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/daemon.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/datastream.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/document.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/gui.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/ilm.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/index.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/index_recipes.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/memory.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/ml.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/painless_commands.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/rag.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/script.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/security.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/snapshot.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/tasks.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/telemetry.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/template.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/tools.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/commands/utils.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/completion.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/cli/output.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/defaults.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/config/loader.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/base.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/client.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/daemon.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/datastream.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/errors.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/ilm.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/index.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/logger.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/middleware.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/query_builder.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/rag/ast_parser.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/rag/ingestor.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/snapshot.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/core/validation.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/assets/index-CeUjjtn-.css +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/assets/index-DNdGuJvV.js +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/elastro.svg +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/favicon.ico +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/index.html +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/gui/vite.svg +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/py.typed +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/aliases.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/health.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/snapshots.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro/utils/templates.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/dependency_links.txt +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/entry_points.txt +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/requires.txt +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/elastro_client.egg-info/top_level.txt +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/client.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/config_usage.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/datastreams.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/debug_connection.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/document_operations.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/index_management.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/examples/search.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/pytest.ini +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/requirements.txt +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/setup.cfg +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/setup.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/conftest.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/datastream_fixtures.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/document_fixtures.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/fixtures/index_fixtures.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_aggregations_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_client_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_datastream_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_docs_quickstart.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_document_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_index_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_query_builder_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_scroll_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/integration/test_workflow_integration.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/manual/test_es.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_aggregations.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_query_builder.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/advanced/test_scroll.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/test_defaults.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/config/test_loader.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_client.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_datastream.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_document.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_errors.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_index.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/core/test_validation.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/__init__.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_aliases.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_health.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_snapshots.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/unit/utils/test_templates.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/verify_api_actions.py +0 -0
- {elastro_client-1.3.53 → elastro_client-1.3.55}/tests/verify_cli_e2e.py +0 -0
|
@@ -4,17 +4,26 @@ Elasticsearch Management Module.
|
|
|
4
4
|
A module for managing Elasticsearch operations within a pipeline process.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
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
|
+
)
|