feat(ingest): Add 7 new data source ingestors

- Add PandasIngestor for DataFrame, CSV, JSON ingestion
- Add DuckDBIngestor for CSV, Parquet, Excel with SQL queries
- Add MongoIngestor for MongoDB document databases
- Add ElasticIngestor for Elasticsearch indices
- Add RESTIngestor for generic REST API endpoints
- Add HuggingFaceIngestor for ML datasets from HuggingFace Hub
- Add GDriveIngestor for Google Drive files and folders

- Update registry, methods, and config for new ingestors
- Add comprehensive documentation and code examples
- Add optional dependencies to pyproject.toml
This commit is contained in:
KaifAhmad1
2025-12-01 23:20:56 +05:30
parent 38af56a011
commit 4364f409a0
7 changed files with 2712 additions and 0 deletions
+372
View File
@@ -0,0 +1,372 @@
"""
REST API Ingestion Module
This module provides comprehensive REST API ingestion capabilities for the
Semantica framework, enabling data extraction from any REST API endpoint.
Key Features:
- Generic REST API client
- Pagination support
- Authentication (API key, OAuth, Bearer token)
- Batch request handling
- Error handling and retry logic
Main Classes:
- RESTIngestor: Main REST API ingestion class
- APIData: Data representation for API ingestion
Example Usage:
>>> from semantica.ingest import RESTIngestor
>>> ingestor = RESTIngestor()
>>> data = ingestor.ingest_endpoint("https://api.example.com/data", headers={"Authorization": "Bearer token"})
>>> paginated_data = ingestor.paginated_fetch("https://api.example.com/data", page_size=100)
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from urllib.parse import urljoin, urlparse
import requests
from requests.adapters import HTTPAdapter
try:
from urllib3.util.retry import Retry
except ImportError:
from requests.packages.urllib3.util.retry import Retry
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
@dataclass
class APIData:
"""REST API data representation."""
data: Union[List[Dict[str, Any]], Dict[str, Any]]
response_status: int
endpoint: str
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class RESTIngestor:
"""
REST API ingestion handler.
This class provides comprehensive REST API ingestion capabilities,
supporting generic API endpoints with authentication, pagination, and
error handling.
Features:
- Generic REST API client
- Pagination support
- Authentication (API key, OAuth, Bearer token)
- Batch request handling
- Error handling and retry logic
Example Usage:
>>> ingestor = RESTIngestor()
>>> data = ingestor.ingest_endpoint("https://api.example.com/data")
>>> paginated_data = ingestor.paginated_fetch("https://api.example.com/data", page_size=100)
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize REST API ingestor.
Args:
config: Optional REST API ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
self.logger = get_logger("api_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize session with retry strategy
self.session = requests.Session()
retry_strategy = Retry(
total=self.config.get("max_retries", 3),
backoff_factor=self.config.get("backoff_factor", 1),
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
# Set default headers
default_headers = self.config.get("headers", {})
if default_headers:
self.session.headers.update(default_headers)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("REST API ingestor initialized")
def ingest_endpoint(
self,
endpoint: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, Any]] = None,
data: Optional[Union[Dict, str]] = None,
json_data: Optional[Dict[str, Any]] = None,
**options,
) -> APIData:
"""
Ingest data from REST API endpoint.
This method makes a request to a REST API endpoint and returns the response data.
Args:
endpoint: API endpoint URL
method: HTTP method (GET, POST, PUT, DELETE, etc.)
headers: Optional request headers
params: Optional query parameters
data: Optional request body (for form data)
json_data: Optional JSON request body
**options: Additional request options
Returns:
APIData: Ingested data object containing:
- data: Response data (parsed JSON or raw text)
- response_status: HTTP status code
- endpoint: Endpoint URL
- metadata: Additional metadata
Raises:
ValidationError: If endpoint is invalid
ProcessingError: If request fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=endpoint,
module="ingest",
submodule="RESTIngestor",
message=f"Requesting: {method} {endpoint}",
)
try:
# Prepare request
request_headers = self.session.headers.copy()
if headers:
request_headers.update(headers)
# Make request
response = self.session.request(
method=method,
url=endpoint,
headers=request_headers,
params=params,
data=data,
json=json_data,
timeout=self.config.get("timeout", 30),
**options,
)
# Check for errors
response.raise_for_status()
# Parse response
try:
response_data = response.json()
except ValueError:
# Not JSON, return as text
response_data = response.text
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Request successful: {response.status_code}",
)
self.logger.info(
f"API request completed: {method} {endpoint} - {response.status_code}"
)
return APIData(
data=response_data,
response_status=response.status_code,
endpoint=endpoint,
metadata={
"method": method,
"headers": dict(response.headers),
"content_type": response.headers.get("Content-Type"),
},
)
except requests.exceptions.RequestException as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest endpoint {endpoint}: {e}")
raise ProcessingError(f"Failed to ingest endpoint: {e}") from e
def paginated_fetch(
self,
endpoint: str,
page_size: int = 100,
page_param: str = "page",
size_param: str = "size",
limit: Optional[int] = None,
**options,
) -> List[APIData]:
"""
Fetch paginated data from REST API endpoint.
This method handles pagination automatically, fetching all pages or up to
a specified limit.
Args:
endpoint: API endpoint URL
page_size: Number of items per page
page_param: Query parameter name for page number
size_param: Query parameter name for page size
limit: Maximum number of items to fetch (optional)
**options: Additional request options
Returns:
List of APIData objects, one per page
Raises:
ProcessingError: If pagination fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=endpoint,
module="ingest",
submodule="RESTIngestor",
message=f"Fetching paginated data from: {endpoint}",
)
try:
all_pages = []
page = 1
total_fetched = 0
while True:
# Build query parameters
params = options.get("params", {}).copy()
params[page_param] = page
params[size_param] = page_size
# Fetch page
page_options = options.copy()
page_options["params"] = params
page_data = self.ingest_endpoint(endpoint, **page_options)
# Extract items from response
if isinstance(page_data.data, list):
items = page_data.data
elif isinstance(page_data.data, dict):
# Try common pagination response formats
items = (
page_data.data.get("items", [])
or page_data.data.get("data", [])
or page_data.data.get("results", [])
or [page_data.data]
)
else:
items = []
if not items:
# No more items, stop pagination
break
all_pages.append(page_data)
total_fetched += len(items)
# Check limit
if limit and total_fetched >= limit:
break
# Check if there are more pages
if isinstance(page_data.data, dict):
has_more = (
page_data.data.get("has_more", False)
or page_data.data.get("next", None) is not None
)
if not has_more:
break
page += 1
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Fetched {len(all_pages)} pages, {total_fetched} items",
)
self.logger.info(
f"Paginated fetch completed: {len(all_pages)} page(s), {total_fetched} item(s)"
)
return all_pages
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to fetch paginated data: {e}")
raise ProcessingError(f"Failed to fetch paginated data: {e}") from e
def batch_request(
self,
endpoints: List[str],
method: str = "GET",
**options,
) -> List[APIData]:
"""
Make batch requests to multiple endpoints.
This method makes requests to multiple endpoints and returns all results.
Args:
endpoints: List of endpoint URLs
method: HTTP method
**options: Additional request options
Returns:
List of APIData objects, one per endpoint
Raises:
ProcessingError: If batch request fails
"""
tracking_id = self.progress_tracker.start_tracking(
file="batch",
module="ingest",
submodule="RESTIngestor",
message=f"Batch request: {len(endpoints)} endpoints",
)
try:
results = []
for endpoint in endpoints:
try:
data = self.ingest_endpoint(endpoint, method=method, **options)
results.append(data)
except Exception as e:
self.logger.warning(f"Failed to fetch {endpoint}: {e}")
if self.config.get("fail_fast", False):
raise
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Batch request completed: {len(results)}/{len(endpoints)} successful",
)
self.logger.info(
f"Batch request completed: {len(results)}/{len(endpoints)} successful"
)
return results
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to execute batch request: {e}")
raise ProcessingError(f"Failed to execute batch request: {e}") from e
+432
View File
@@ -0,0 +1,432 @@
"""
DuckDB Ingestion Module
This module provides comprehensive DuckDB ingestion capabilities for the
Semantica framework, enabling data extraction from CSV, Parquet, and Excel files
using DuckDB's SQL interface.
Key Features:
- CSV file ingestion with SQL queries
- Parquet file ingestion
- Excel file ingestion
- SQL query execution on files
- Schema extraction
- Large dataset handling
Main Classes:
- DuckDBIngestor: Main DuckDB ingestion class
- DuckDBData: Data representation for DuckDB ingestion
Example Usage:
>>> from semantica.ingest import DuckDBIngestor
>>> ingestor = DuckDBIngestor()
>>> data = ingestor.ingest_csv("data.csv")
>>> parquet_data = ingestor.ingest_parquet("data.parquet")
>>> excel_data = ingestor.ingest_excel("data.xlsx")
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
import duckdb
except ImportError:
duckdb = None
@dataclass
class DuckDBData:
"""DuckDB data representation."""
data: List[Dict[str, Any]]
row_count: int
columns: List[str]
query: Optional[str] = None
source_file: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class DuckDBIngestor:
"""
DuckDB ingestion handler.
This class provides comprehensive DuckDB ingestion capabilities,
supporting ingestion from CSV, Parquet, and Excel files using DuckDB's
SQL interface for efficient querying.
Features:
- CSV file ingestion
- Parquet file ingestion
- Excel file ingestion
- SQL query execution on files
- Schema extraction
- Large dataset handling
Example Usage:
>>> ingestor = DuckDBIngestor()
>>> data = ingestor.ingest_csv("data.csv")
>>> result = ingestor.execute_query("SELECT * FROM 'data.csv' LIMIT 100")
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize DuckDB ingestor.
Args:
config: Optional DuckDB ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
if duckdb is None:
raise ImportError(
"duckdb is required for DuckDBIngestor. Install it with: pip install duckdb"
)
self.logger = get_logger("duckdb_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize DuckDB connection
self.conn = duckdb.connect()
if self.config.get("memory_limit"):
self.conn.execute(f"SET memory_limit='{self.config['memory_limit']}'")
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("DuckDB ingestor initialized")
def __del__(self):
"""Close DuckDB connection on cleanup."""
if hasattr(self, "conn") and self.conn:
try:
self.conn.close()
except Exception:
pass
def ingest_csv(
self,
file_path: Union[str, Path],
limit: Optional[int] = None,
where: Optional[str] = None,
**options,
) -> DuckDBData:
"""
Ingest data from CSV file.
This method reads a CSV file using DuckDB and returns the data.
Args:
file_path: Path to CSV file
limit: Maximum number of rows to return (optional)
where: WHERE clause for filtering (optional)
**options: Additional query options
Returns:
DuckDBData: Ingested data object
Raises:
ValidationError: If CSV file not found
ProcessingError: If CSV reading fails
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(f"CSV file not found: {file_path}")
tracking_id = self.progress_tracker.start_tracking(
file=str(file_path),
module="ingest",
submodule="DuckDBIngestor",
message=f"Ingesting CSV: {file_path.name}",
)
try:
# Build SQL query
query = f"SELECT * FROM read_csv_auto('{file_path}')"
if where:
query += f" WHERE {where}"
if limit:
query += f" LIMIT {limit}"
# Execute query
result = self.conn.execute(query).fetchall()
columns = [desc[0] for desc in self.conn.description]
# Convert to list of dictionaries
data = [dict(zip(columns, row)) for row in result]
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested CSV: {len(data)} rows",
)
self.logger.info(f"CSV ingestion completed: {len(data)} row(s)")
return DuckDBData(
data=data,
row_count=len(data),
columns=columns,
query=query,
source_file=str(file_path),
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest CSV {file_path}: {e}")
raise ProcessingError(f"Failed to ingest CSV: {e}") from e
def ingest_parquet(
self,
file_path: Union[str, Path],
limit: Optional[int] = None,
where: Optional[str] = None,
**options,
) -> DuckDBData:
"""
Ingest data from Parquet file.
This method reads a Parquet file using DuckDB and returns the data.
Args:
file_path: Path to Parquet file
limit: Maximum number of rows to return (optional)
where: WHERE clause for filtering (optional)
**options: Additional query options
Returns:
DuckDBData: Ingested data object
Raises:
ValidationError: If Parquet file not found
ProcessingError: If Parquet reading fails
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(f"Parquet file not found: {file_path}")
tracking_id = self.progress_tracker.start_tracking(
file=str(file_path),
module="ingest",
submodule="DuckDBIngestor",
message=f"Ingesting Parquet: {file_path.name}",
)
try:
# Build SQL query
query = f"SELECT * FROM read_parquet('{file_path}')"
if where:
query += f" WHERE {where}"
if limit:
query += f" LIMIT {limit}"
# Execute query
result = self.conn.execute(query).fetchall()
columns = [desc[0] for desc in self.conn.description]
# Convert to list of dictionaries
data = [dict(zip(columns, row)) for row in result]
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested Parquet: {len(data)} rows",
)
self.logger.info(f"Parquet ingestion completed: {len(data)} row(s)")
return DuckDBData(
data=data,
row_count=len(data),
columns=columns,
query=query,
source_file=str(file_path),
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest Parquet {file_path}: {e}")
raise ProcessingError(f"Failed to ingest Parquet: {e}") from e
def ingest_excel(
self,
file_path: Union[str, Path],
sheet_name: Optional[str] = None,
limit: Optional[int] = None,
where: Optional[str] = None,
**options,
) -> DuckDBData:
"""
Ingest data from Excel file.
This method reads an Excel file using DuckDB (via Parquet conversion)
and returns the data.
Args:
file_path: Path to Excel file
sheet_name: Name of sheet to read (optional, reads first sheet if not specified)
limit: Maximum number of rows to return (optional)
where: WHERE clause for filtering (optional)
**options: Additional query options
Returns:
DuckDBData: Ingested data object
Raises:
ValidationError: If Excel file not found
ProcessingError: If Excel reading fails
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(f"Excel file not found: {file_path}")
tracking_id = self.progress_tracker.start_tracking(
file=str(file_path),
module="ingest",
submodule="DuckDBIngestor",
message=f"Ingesting Excel: {file_path.name}",
)
try:
# DuckDB doesn't directly support Excel, so we need to use pandas
# to read Excel and then query it
try:
import pandas as pd
except ImportError:
raise ImportError(
"pandas and openpyxl are required for Excel ingestion. "
"Install with: pip install pandas openpyxl"
)
# Read Excel with pandas
if sheet_name:
df = pd.read_excel(file_path, sheet_name=sheet_name, **options)
else:
df = pd.read_excel(file_path, **options)
# Register DataFrame as a DuckDB table
table_name = f"excel_data_{id(df)}"
self.conn.register(table_name, df)
# Build SQL query
query = f"SELECT * FROM {table_name}"
if where:
query += f" WHERE {where}"
if limit:
query += f" LIMIT {limit}"
# Execute query
result = self.conn.execute(query).fetchall()
columns = [desc[0] for desc in self.conn.description]
# Convert to list of dictionaries
data = [dict(zip(columns, row)) for row in result]
# Unregister table
self.conn.unregister(table_name)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested Excel: {len(data)} rows",
)
self.logger.info(f"Excel ingestion completed: {len(data)} row(s)")
return DuckDBData(
data=data,
row_count=len(data),
columns=columns,
query=query,
source_file=str(file_path),
metadata={"sheet_name": sheet_name},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest Excel {file_path}: {e}")
raise ProcessingError(f"Failed to ingest Excel: {e}") from e
def execute_query(
self,
query: str,
file_path: Optional[Union[str, Path]] = None,
**options,
) -> DuckDBData:
"""
Execute SQL query on file or in-memory data.
This method executes a SQL query using DuckDB. If file_path is provided,
the query can reference the file directly.
Args:
query: SQL query to execute
file_path: Optional file path to reference in query
**options: Additional query options
Returns:
DuckDBData: Query result data object
Raises:
ProcessingError: If query execution fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=file_path or "query",
module="ingest",
submodule="DuckDBIngestor",
message="Executing SQL query",
)
try:
# Execute query
result = self.conn.execute(query).fetchall()
columns = [desc[0] for desc in self.conn.description]
# Convert to list of dictionaries
data = [dict(zip(columns, row)) for row in result]
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Query executed: {len(data)} rows",
)
self.logger.info(f"Query executed: {len(data)} row(s) returned")
return DuckDBData(
data=data,
row_count=len(data),
columns=columns,
query=query,
source_file=str(file_path) if file_path else None,
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to execute query: {e}")
raise ProcessingError(f"Failed to execute query: {e}") from e
+356
View File
@@ -0,0 +1,356 @@
"""
Elasticsearch Ingestion Module
This module provides comprehensive Elasticsearch ingestion capabilities for the
Semantica framework, enabling data extraction from Elasticsearch indices.
Key Features:
- Index ingestion
- Search query execution
- Index export
- Schema extraction
- Large dataset handling with scroll API
Main Classes:
- ElasticIngestor: Main Elasticsearch ingestion class
- ElasticData: Data representation for Elasticsearch ingestion
Example Usage:
>>> from semantica.ingest import ElasticIngestor
>>> ingestor = ElasticIngestor()
>>> data = ingestor.ingest_index("http://localhost:9200", "my_index")
>>> results = ingestor.search_documents("http://localhost:9200", "my_index", {"query": {"match_all": {}}})
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
from elasticsearch import Elasticsearch
from elasticsearch.helpers import scan
except ImportError:
Elasticsearch = None
scan = None
@dataclass
class ElasticData:
"""Elasticsearch data representation."""
documents: List[Dict[str, Any]]
document_count: int
index_name: str
schema: Dict[str, Any]
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class ElasticIngestor:
"""
Elasticsearch ingestion handler.
This class provides comprehensive Elasticsearch ingestion capabilities,
connecting to Elasticsearch, querying indices, and exporting data.
Features:
- Index ingestion
- Search query execution
- Index export
- Schema extraction
- Large dataset handling with scroll API
Example Usage:
>>> ingestor = ElasticIngestor()
>>> data = ingestor.ingest_index("http://localhost:9200", "my_index")
>>> results = ingestor.search_documents("http://localhost:9200", "my_index", {"query": {"match_all": {}}})
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize Elasticsearch ingestor.
Args:
config: Optional Elasticsearch ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
if Elasticsearch is None:
raise ImportError(
"elasticsearch is required for ElasticIngestor. Install it with: pip install elasticsearch"
)
self.logger = get_logger("elastic_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("Elasticsearch ingestor initialized")
def _get_client(self, connection_string: str) -> Elasticsearch:
"""
Get Elasticsearch client.
Args:
connection_string: Elasticsearch connection string or host
Returns:
Elasticsearch: Elasticsearch client object
"""
# Parse connection string
if connection_string.startswith("http://") or connection_string.startswith(
"https://"
):
hosts = [connection_string]
else:
# Assume it's a host:port format
hosts = [connection_string]
# Create client
client_config = self.config.get("client_config", {})
client = Elasticsearch(hosts=hosts, **client_config)
# Test connection
if not client.ping():
raise ProcessingError("Failed to connect to Elasticsearch")
return client
def ingest_index(
self,
connection_string: str,
index_name: str,
limit: Optional[int] = None,
**options,
) -> ElasticData:
"""
Ingest data from Elasticsearch index.
This method connects to Elasticsearch, retrieves documents from an index,
and extracts schema information.
Args:
connection_string: Elasticsearch connection string (e.g., "http://localhost:9200")
index_name: Name of the index
limit: Maximum number of documents to retrieve (optional)
**options: Additional processing options
Returns:
ElasticData: Ingested data object containing:
- documents: List of document dictionaries
- document_count: Number of documents
- index_name: Index name
- schema: Schema information dictionary
Raises:
ProcessingError: If ingestion fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=index_name,
module="ingest",
submodule="ElasticIngestor",
message=f"Ingesting index: {index_name}",
)
try:
# Get client
client = self._get_client(connection_string)
# Check if index exists
if not client.indices.exists(index=index_name):
raise ValidationError(f"Index not found: {index_name}")
# Get total document count
count_response = client.count(index=index_name)
total_count = count_response["count"]
# Retrieve documents using scroll API for large datasets
self.progress_tracker.update_tracking(
tracking_id, message=f"Retrieving documents (total: {total_count})..."
)
query = options.get("query", {"match_all": {}})
scroll_size = options.get("scroll_size", 1000)
documents = []
if limit:
# Use regular search with size limit
response = client.search(
index=index_name, body={"query": query}, size=limit
)
documents = [hit["_source"] for hit in response["hits"]["hits"]]
else:
# Use scroll API for all documents
for doc in scan(
client, query={"query": query}, index=index_name, size=scroll_size
):
documents.append(doc["_source"])
if limit and len(documents) >= limit:
break
# Extract schema information
schema = self._extract_schema(documents)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(documents)} documents",
)
self.logger.info(
f"Index ingestion completed: {len(documents)} document(s)"
)
return ElasticData(
documents=documents,
document_count=len(documents),
index_name=index_name,
schema=schema,
metadata={"total_count": total_count, "query": query},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest index: {e}")
raise ProcessingError(f"Failed to ingest index: {e}") from e
def search_documents(
self,
connection_string: str,
index_name: str,
search_query: Dict[str, Any],
limit: Optional[int] = None,
**options,
) -> ElasticData:
"""
Search documents in Elasticsearch index.
This method executes a search query on an Elasticsearch index and returns
matching documents.
Args:
connection_string: Elasticsearch connection string
index_name: Name of the index
search_query: Elasticsearch search query dictionary
limit: Maximum number of documents to return (optional)
**options: Additional search options
Returns:
ElasticData: Search result data object
Raises:
ProcessingError: If search fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=index_name,
module="ingest",
submodule="ElasticIngestor",
message=f"Searching index: {index_name}",
)
try:
# Get client
client = self._get_client(connection_string)
# Execute search
size = limit or options.get("size", 100)
response = client.search(
index=index_name, body=search_query, size=size, **options
)
# Extract documents
documents = [hit["_source"] for hit in response["hits"]["hits"]]
# Extract schema information
schema = self._extract_schema(documents)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Search returned {len(documents)} documents",
)
self.logger.info(f"Search completed: {len(documents)} document(s)")
return ElasticData(
documents=documents,
document_count=len(documents),
index_name=index_name,
schema=schema,
metadata={
"total_hits": response["hits"]["total"]["value"],
"query": search_query,
},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to search documents: {e}")
raise ProcessingError(f"Failed to search documents: {e}") from e
def export_index(
self,
connection_string: str,
index_name: str,
**options,
) -> ElasticData:
"""
Export entire index.
This method exports all documents from an Elasticsearch index.
Args:
connection_string: Elasticsearch connection string
index_name: Name of the index
**options: Additional export options
Returns:
ElasticData: Exported data object
Raises:
ProcessingError: If export fails
"""
return self.ingest_index(connection_string, index_name, limit=None, **options)
def _extract_schema(self, documents: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Extract schema information from documents.
Args:
documents: List of Elasticsearch documents
Returns:
Dictionary containing schema information
"""
if not documents:
return {"fields": [], "field_types": {}}
# Collect all field names and types
field_types = {}
for doc in documents:
for key, value in doc.items():
if key not in field_types:
field_types[key] = set()
field_types[key].add(type(value).__name__)
# Convert sets to lists
schema = {
"fields": list(field_types.keys()),
"field_types": {
k: list(v) if len(v) > 1 else list(v)[0]
for k, v in field_types.items()
},
}
return schema
+422
View File
@@ -0,0 +1,422 @@
"""
Google Drive Ingestion Module
This module provides comprehensive Google Drive ingestion capabilities for the
Semantica framework, enabling data extraction from Google Drive files and folders.
Key Features:
- Folder ingestion
- File ingestion
- Drive export
- File type detection
- OAuth authentication
Main Classes:
- GDriveIngestor: Main Google Drive ingestion class
- GDriveData: Data representation for Google Drive ingestion
Example Usage:
>>> from semantica.ingest import GDriveIngestor
>>> ingestor = GDriveIngestor(credentials_path="credentials.json")
>>> data = ingestor.ingest_folder("folder_id")
>>> file_data = ingestor.ingest_file("file_id")
"""
import json
import os
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaIoBaseDownload
import io
except ImportError:
Credentials = None
InstalledAppFlow = None
Request = None
build = None
HttpError = None
MediaIoBaseDownload = None
io = None
@dataclass
class GDriveData:
"""Google Drive data representation."""
files: List[Dict[str, Any]]
file_count: int
folder_id: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class GDriveIngestor:
"""
Google Drive ingestion handler.
This class provides comprehensive Google Drive ingestion capabilities,
connecting to Google Drive, listing files, and downloading content.
Features:
- Folder ingestion
- File ingestion
- Drive export
- File type detection
- OAuth authentication
Example Usage:
>>> ingestor = GDriveIngestor(credentials_path="credentials.json")
>>> data = ingestor.ingest_folder("folder_id")
"""
# Google Drive API scopes
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize Google Drive ingestor.
Args:
config: Optional Google Drive ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
if build is None:
raise ImportError(
"google-api-python-client and google-auth-oauthlib are required for GDriveIngestor. "
"Install with: pip install google-api-python-client google-auth-oauthlib"
)
self.logger = get_logger("gdrive_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize Google Drive service
self.service = None
self.credentials_path = self.config.get("credentials_path")
self.token_path = self.config.get("token_path", "token.json")
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("Google Drive ingestor initialized")
def _authenticate(self):
"""
Authenticate with Google Drive API.
Raises:
ProcessingError: If authentication fails
"""
if self.service:
return
creds = None
# Load existing token
if os.path.exists(self.token_path):
try:
creds = Credentials.from_authorized_user_file(
self.token_path, self.SCOPES
)
except Exception as e:
self.logger.warning(f"Failed to load token: {e}")
# If there are no (valid) credentials available, let the user log in
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
if not self.credentials_path:
raise ValidationError(
"credentials_path is required for Google Drive authentication. "
"Provide path to OAuth2 credentials JSON file."
)
if not os.path.exists(self.credentials_path):
raise ValidationError(
f"Credentials file not found: {self.credentials_path}"
)
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path, self.SCOPES
)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(self.token_path, "w") as token:
token.write(creds.to_json())
# Build service
self.service = build("drive", "v3", credentials=creds)
self.logger.info("Authenticated with Google Drive")
def ingest_folder(
self,
folder_id: str,
include_subfolders: bool = False,
file_types: Optional[List[str]] = None,
**options,
) -> GDriveData:
"""
Ingest data from Google Drive folder.
This method lists all files in a Google Drive folder and retrieves
their metadata.
Args:
folder_id: Google Drive folder ID
include_subfolders: Whether to include files from subfolders
file_types: Optional list of file MIME types to filter
**options: Additional processing options
Returns:
GDriveData: Ingested data object containing:
- files: List of file metadata dictionaries
- file_count: Number of files
- folder_id: Folder ID
- metadata: Additional metadata
Raises:
ProcessingError: If ingestion fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=folder_id,
module="ingest",
submodule="GDriveIngestor",
message=f"Ingesting folder: {folder_id}",
)
try:
# Authenticate
self._authenticate()
# Query files in folder
query = f"'{folder_id}' in parents and trashed=false"
if file_types:
mime_types = " or ".join([f"mimeType='{ft}'" for ft in file_types])
query += f" and ({mime_types})"
self.progress_tracker.update_tracking(
tracking_id, message="Listing files in folder..."
)
files = []
page_token = None
while True:
results = (
self.service.files()
.list(
q=query,
pageSize=1000,
fields="nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime)",
pageToken=page_token,
)
.execute()
)
items = results.get("files", [])
files.extend(items)
page_token = results.get("nextPageToken")
if not page_token:
break
# Handle subfolders if requested
if include_subfolders:
# Get all subfolders
subfolder_query = f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
subfolders = []
page_token = None
while True:
results = (
self.service.files()
.list(
q=subfolder_query,
pageSize=1000,
fields="nextPageToken, files(id, name)",
pageToken=page_token,
)
.execute()
)
items = results.get("files", [])
subfolders.extend(items)
page_token = results.get("nextPageToken")
if not page_token:
break
# Recursively ingest subfolders
for subfolder in subfolders:
try:
subfolder_data = self.ingest_folder(
subfolder["id"],
include_subfolders=True,
file_types=file_types,
**options,
)
files.extend(subfolder_data.files)
except Exception as e:
self.logger.warning(
f"Failed to ingest subfolder {subfolder['name']}: {e}"
)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(files)} files",
)
self.logger.info(f"Folder ingestion completed: {len(files)} file(s)")
return GDriveData(
files=files,
file_count=len(files),
folder_id=folder_id,
metadata={"include_subfolders": include_subfolders},
)
except HttpError as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest folder: {e}")
raise ProcessingError(f"Failed to ingest folder: {e}") from e
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest folder: {e}")
raise ProcessingError(f"Failed to ingest folder: {e}") from e
def ingest_file(
self,
file_id: str,
download: bool = False,
**options,
) -> Dict[str, Any]:
"""
Ingest data from Google Drive file.
This method retrieves metadata and optionally downloads content
from a Google Drive file.
Args:
file_id: Google Drive file ID
download: Whether to download file content
**options: Additional processing options
Returns:
Dictionary containing file metadata and optionally content
Raises:
ProcessingError: If ingestion fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=file_id,
module="ingest",
submodule="GDriveIngestor",
message=f"Ingesting file: {file_id}",
)
try:
# Authenticate
self._authenticate()
# Get file metadata
file_metadata = (
self.service.files().get(fileId=file_id, fields="*").execute()
)
result = {"metadata": file_metadata}
# Download content if requested
if download:
self.progress_tracker.update_tracking(
tracking_id, message="Downloading file content..."
)
# Check if file is Google Workspace file (needs export)
mime_type = file_metadata.get("mimeType", "")
if mime_type.startswith("application/vnd.google-apps"):
# Export Google Workspace file
export_mime_type = options.get(
"export_mime_type", "application/pdf"
)
request = self.service.files().export_media(
fileId=file_id, mimeType=export_mime_type
)
else:
# Download regular file
request = self.service.files().get_media(fileId=file_id)
# Download to bytes
file_content = io.BytesIO()
downloader = MediaIoBaseDownload(file_content, request)
done = False
while not done:
status, done = downloader.next_chunk()
result["content"] = file_content.getvalue()
self.progress_tracker.stop_tracking(
tracking_id, status="completed", message="File ingested"
)
self.logger.info(f"File ingestion completed: {file_id}")
return result
except HttpError as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest file: {e}")
raise ProcessingError(f"Failed to ingest file: {e}") from e
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest file: {e}")
raise ProcessingError(f"Failed to ingest file: {e}") from e
def export_drive(
self,
folder_id: Optional[str] = None,
**options,
) -> GDriveData:
"""
Export entire Google Drive or a folder.
This method exports all files from a Google Drive folder or the entire drive.
Args:
folder_id: Optional folder ID (exports entire drive if not provided)
**options: Additional export options
Returns:
GDriveData: Exported data object
Raises:
ProcessingError: If export fails
"""
if folder_id:
return self.ingest_folder(folder_id, include_subfolders=True, **options)
else:
# Export entire drive (root folder)
return self.ingest_folder("root", include_subfolders=True, **options)
+319
View File
@@ -0,0 +1,319 @@
"""
HuggingFace Ingestion Module
This module provides comprehensive HuggingFace datasets ingestion capabilities for the
Semantica framework, enabling data extraction from HuggingFace Hub datasets.
Key Features:
- Dataset ingestion
- Streaming dataset support
- Split export
- Large dataset handling
Main Classes:
- HuggingFaceIngestor: Main HuggingFace ingestion class
- HFData: Data representation for HuggingFace ingestion
Example Usage:
>>> from semantica.ingest import HuggingFaceIngestor
>>> ingestor = HuggingFaceIngestor()
>>> data = ingestor.ingest_dataset("squad", split="train")
>>> stream_data = ingestor.stream_dataset("squad", split="train")
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
from datasets import load_dataset, Dataset, IterableDataset
except ImportError:
load_dataset = None
Dataset = None
IterableDataset = None
@dataclass
class HFData:
"""HuggingFace data representation."""
data: List[Dict[str, Any]]
row_count: int
columns: List[str]
dataset_name: str
split: Optional[str] = None
schema: Dict[str, Any] = field(default_factory=dict)
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class HuggingFaceIngestor:
"""
HuggingFace ingestion handler.
This class provides comprehensive HuggingFace datasets ingestion capabilities,
loading datasets from HuggingFace Hub and converting them to standard formats.
Features:
- Dataset ingestion
- Streaming dataset support
- Split export
- Large dataset handling
Example Usage:
>>> ingestor = HuggingFaceIngestor()
>>> data = ingestor.ingest_dataset("squad", split="train")
>>> stream_data = ingestor.stream_dataset("squad", split="train")
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize HuggingFace ingestor.
Args:
config: Optional HuggingFace ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
if load_dataset is None:
raise ImportError(
"datasets is required for HuggingFaceIngestor. Install it with: pip install datasets"
)
self.logger = get_logger("huggingface_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("HuggingFace ingestor initialized")
def ingest_dataset(
self,
dataset_name: str,
split: Optional[str] = None,
limit: Optional[int] = None,
**options,
) -> HFData:
"""
Ingest data from HuggingFace dataset.
This method loads a dataset from HuggingFace Hub and converts it to
a standard format.
Args:
dataset_name: Name of the dataset (e.g., "squad", "glue")
split: Dataset split to load (e.g., "train", "test", "validation")
limit: Maximum number of rows to return (optional)
**options: Additional options passed to load_dataset()
Returns:
HFData: Ingested data object containing:
- data: List of row dictionaries
- row_count: Number of rows
- columns: List of column names
- dataset_name: Dataset name
- split: Split name
- schema: Schema information
Raises:
ValidationError: If dataset not found
ProcessingError: If ingestion fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=dataset_name,
module="ingest",
submodule="HuggingFaceIngestor",
message=f"Loading dataset: {dataset_name}",
)
try:
# Load dataset
self.progress_tracker.update_tracking(
tracking_id, message="Loading from HuggingFace Hub..."
)
if split:
dataset = load_dataset(dataset_name, split=split, **options)
else:
# Load all splits
dataset = load_dataset(dataset_name, **options)
# Use first split if multiple splits
if isinstance(dataset, dict):
split = list(dataset.keys())[0]
dataset = dataset[split]
# Convert to list of dictionaries
self.progress_tracker.update_tracking(
tracking_id, message="Converting to list format..."
)
if limit:
data = [dataset[i] for i in range(min(limit, len(dataset)))]
else:
data = list(dataset)
# Extract columns and schema
columns = list(data[0].keys()) if data else []
schema = {
"columns": columns,
"features": dataset.features.to_dict() if hasattr(dataset, "features") else {},
}
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(data)} rows",
)
self.logger.info(
f"Dataset ingestion completed: {len(data)} row(s) from {dataset_name}"
)
return HFData(
data=data,
row_count=len(data),
columns=columns,
dataset_name=dataset_name,
split=split,
schema=schema,
metadata={"features": schema.get("features", {})},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest dataset {dataset_name}: {e}")
raise ProcessingError(f"Failed to ingest dataset: {e}") from e
def stream_dataset(
self,
dataset_name: str,
split: Optional[str] = None,
limit: Optional[int] = None,
**options,
) -> HFData:
"""
Stream dataset from HuggingFace Hub.
This method loads a dataset in streaming mode, which is more memory-efficient
for large datasets.
Args:
dataset_name: Name of the dataset
split: Dataset split to load (optional)
limit: Maximum number of rows to return (optional)
**options: Additional options passed to load_dataset()
Returns:
HFData: Streamed data object
Raises:
ProcessingError: If streaming fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=dataset_name,
module="ingest",
submodule="HuggingFaceIngestor",
message=f"Streaming dataset: {dataset_name}",
)
try:
# Load dataset in streaming mode
self.progress_tracker.update_tracking(
tracking_id, message="Loading in streaming mode..."
)
streaming_options = options.copy()
streaming_options["streaming"] = True
if split:
dataset = load_dataset(
dataset_name, split=split, **streaming_options
)
else:
dataset = load_dataset(dataset_name, **streaming_options)
if isinstance(dataset, dict):
split = list(dataset.keys())[0]
dataset = dataset[split]
# Convert streaming dataset to list
self.progress_tracker.update_tracking(
tracking_id, message="Streaming and converting data..."
)
data = []
columns = None
for i, item in enumerate(dataset):
if columns is None:
columns = list(item.keys())
data.append(item)
if limit and len(data) >= limit:
break
# Extract schema
schema = {
"columns": columns or [],
"features": dataset.features.to_dict() if hasattr(dataset, "features") else {},
}
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Streamed {len(data)} rows",
)
self.logger.info(
f"Dataset streaming completed: {len(data)} row(s) from {dataset_name}"
)
return HFData(
data=data,
row_count=len(data),
columns=columns or [],
dataset_name=dataset_name,
split=split,
schema=schema,
metadata={"streaming": True},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to stream dataset {dataset_name}: {e}")
raise ProcessingError(f"Failed to stream dataset: {e}") from e
def export_split(
self,
dataset_name: str,
split: str,
**options,
) -> HFData:
"""
Export a specific split from a dataset.
This method exports a specific split from a HuggingFace dataset.
Args:
dataset_name: Name of the dataset
split: Split name to export
**options: Additional options
Returns:
HFData: Exported split data object
Raises:
ProcessingError: If export fails
"""
return self.ingest_dataset(dataset_name, split=split, **options)
+470
View File
@@ -0,0 +1,470 @@
"""
MongoDB Ingestion Module
This module provides comprehensive MongoDB ingestion capabilities for the
Semantica framework, enabling data extraction from MongoDB collections and databases.
Key Features:
- Collection ingestion
- Query-based document retrieval
- Database export
- Schema extraction
- Large dataset handling with pagination
Main Classes:
- MongoIngestor: Main MongoDB ingestion class
- MongoConnector: MongoDB connection handler
- MongoData: Data representation for MongoDB ingestion
Example Usage:
>>> from semantica.ingest import MongoIngestor
>>> ingestor = MongoIngestor()
>>> data = ingestor.ingest_collection("mongodb://localhost:27017", "mydb", "mycollection")
>>> docs = ingestor.query_documents("mongodb://...", "mydb", "mycollection", {"status": "active"})
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure, OperationFailure
except ImportError:
MongoClient = None
ConnectionFailure = None
OperationFailure = None
@dataclass
class MongoData:
"""MongoDB data representation."""
documents: List[Dict[str, Any]]
document_count: int
collection_name: str
database_name: str
schema: Dict[str, Any]
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class MongoConnector:
"""
MongoDB connection management.
This class manages connections to MongoDB, handles connection pooling,
and provides a unified interface for database operations.
Example Usage:
>>> connector = MongoConnector()
>>> client = connector.connect("mongodb://localhost:27017")
>>> connector.disconnect()
"""
def __init__(self, **config):
"""
Initialize MongoDB connector.
Args:
**config: Connection configuration options
"""
if MongoClient is None:
raise ImportError(
"pymongo is required for MongoConnector. Install it with: pip install pymongo"
)
self.logger = get_logger("mongo_connector")
self.config = config
self.client: Optional[MongoClient] = None
self.logger.debug("MongoDB connector initialized")
def connect(self, connection_string: str) -> MongoClient:
"""
Establish MongoDB connection.
Args:
connection_string: MongoDB connection string
Example: "mongodb://localhost:27017"
Returns:
MongoClient: MongoDB client object
Raises:
ProcessingError: If connection fails
"""
try:
self.client = MongoClient(connection_string, **self.config)
# Test connection
self.client.admin.command("ping")
self.logger.info("Connected to MongoDB")
return self.client
except Exception as e:
self.logger.error(f"Failed to connect to MongoDB: {e}")
raise ProcessingError(f"Failed to connect to MongoDB: {e}") from e
def disconnect(self):
"""Close MongoDB connection."""
if self.client:
self.client.close()
self.client = None
self.logger.info("Disconnected from MongoDB")
def test_connection(self, connection_string: str) -> bool:
"""
Test MongoDB connection without creating a persistent connection.
Args:
connection_string: MongoDB connection string to test
Returns:
bool: True if connection successful, False otherwise
"""
try:
client = MongoClient(connection_string, serverSelectionTimeoutMS=5000)
client.admin.command("ping")
client.close()
return True
except Exception as e:
self.logger.debug(f"Connection test failed: {e}")
return False
class MongoIngestor:
"""
MongoDB ingestion handler.
This class provides comprehensive MongoDB ingestion capabilities,
connecting to MongoDB, querying collections, and exporting data.
Features:
- Collection ingestion
- Query-based document retrieval
- Database export
- Schema extraction
- Large dataset handling with pagination
Example Usage:
>>> ingestor = MongoIngestor()
>>> data = ingestor.ingest_collection("mongodb://...", "mydb", "mycollection")
>>> docs = ingestor.query_documents("mongodb://...", "mydb", "mycollection", {"status": "active"})
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize MongoDB ingestor.
Args:
config: Optional MongoDB ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
self.logger = get_logger("mongo_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize connector
self.connector = MongoConnector(**self.config)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("MongoDB ingestor initialized")
def ingest_collection(
self,
connection_string: str,
database_name: str,
collection_name: str,
limit: Optional[int] = None,
**options,
) -> MongoData:
"""
Ingest data from MongoDB collection.
This method connects to MongoDB, retrieves documents from a collection,
and extracts schema information.
Args:
connection_string: MongoDB connection string
database_name: Name of the database
collection_name: Name of the collection
limit: Maximum number of documents to retrieve (optional)
**options: Additional processing options
Returns:
MongoData: Ingested data object containing:
- documents: List of document dictionaries
- document_count: Number of documents
- collection_name: Collection name
- database_name: Database name
- schema: Schema information dictionary
Raises:
ProcessingError: If ingestion fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=f"{database_name}.{collection_name}",
module="ingest",
submodule="MongoIngestor",
message=f"Collection: {database_name}.{collection_name}",
)
try:
# Connect to MongoDB
client = self.connector.connect(connection_string)
db = client[database_name]
collection = db[collection_name]
# Get document count
total_count = collection.count_documents({})
# Retrieve documents
self.progress_tracker.update_tracking(
tracking_id, message=f"Retrieving documents (total: {total_count})..."
)
query = options.get("query", {})
cursor = collection.find(query)
if limit:
cursor = cursor.limit(limit)
documents = list(cursor)
# Extract schema information
schema = self._extract_schema(documents)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(documents)} documents",
)
self.logger.info(
f"Collection ingestion completed: {len(documents)} document(s)"
)
return MongoData(
documents=documents,
document_count=len(documents),
collection_name=collection_name,
database_name=database_name,
schema=schema,
metadata={"total_count": total_count, "query": query},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest collection: {e}")
raise ProcessingError(f"Failed to ingest collection: {e}") from e
finally:
self.connector.disconnect()
def query_documents(
self,
connection_string: str,
database_name: str,
collection_name: str,
query: Dict[str, Any],
limit: Optional[int] = None,
**options,
) -> MongoData:
"""
Query documents from MongoDB collection.
This method executes a query on a MongoDB collection and returns
matching documents.
Args:
connection_string: MongoDB connection string
database_name: Name of the database
collection_name: Name of the collection
query: MongoDB query dictionary
limit: Maximum number of documents to return (optional)
**options: Additional query options
Returns:
MongoData: Query result data object
Raises:
ProcessingError: If query fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=f"{database_name}.{collection_name}",
module="ingest",
submodule="MongoIngestor",
message=f"Querying collection: {database_name}.{collection_name}",
)
try:
# Connect to MongoDB
client = self.connector.connect(connection_string)
db = client[database_name]
collection = db[collection_name]
# Execute query
cursor = collection.find(query)
if limit:
cursor = cursor.limit(limit)
if "sort" in options:
cursor = cursor.sort(options["sort"])
documents = list(cursor)
# Extract schema information
schema = self._extract_schema(documents)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Query returned {len(documents)} documents",
)
self.logger.info(f"Query completed: {len(documents)} document(s)")
return MongoData(
documents=documents,
document_count=len(documents),
collection_name=collection_name,
database_name=database_name,
schema=schema,
metadata={"query": query},
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to query documents: {e}")
raise ProcessingError(f"Failed to query documents: {e}") from e
finally:
self.connector.disconnect()
def export_database(
self,
connection_string: str,
database_name: str,
include_collections: Optional[List[str]] = None,
exclude_collections: Optional[List[str]] = None,
**options,
) -> Dict[str, MongoData]:
"""
Export entire database.
This method exports all collections from a MongoDB database.
Args:
connection_string: MongoDB connection string
database_name: Name of the database
include_collections: List of collection names to include (optional)
exclude_collections: List of collection names to exclude (optional)
**options: Additional export options
Returns:
Dictionary mapping collection names to MongoData objects
Raises:
ProcessingError: If export fails
"""
tracking_id = self.progress_tracker.start_tracking(
file=database_name,
module="ingest",
submodule="MongoIngestor",
message=f"Exporting database: {database_name}",
)
try:
# Connect to MongoDB
client = self.connector.connect(connection_string)
db = client[database_name]
# Get all collection names
all_collections = db.list_collection_names()
# Apply filters
if include_collections:
collections = [c for c in all_collections if c in include_collections]
else:
exclude_collections = exclude_collections or []
collections = [c for c in all_collections if c not in exclude_collections]
self.progress_tracker.update_tracking(
tracking_id, message=f"Exporting {len(collections)} collections..."
)
# Export each collection
result = {}
for collection_name in collections:
try:
data = self.ingest_collection(
connection_string, database_name, collection_name, **options
)
result[collection_name] = data
except Exception as e:
self.logger.error(
f"Failed to export collection {collection_name}: {e}"
)
if self.config.get("fail_fast", False):
raise
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Exported {len(result)} collections",
)
self.logger.info(f"Database export completed: {len(result)} collection(s)")
return result
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to export database: {e}")
raise ProcessingError(f"Failed to export database: {e}") from e
finally:
self.connector.disconnect()
def _extract_schema(self, documents: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Extract schema information from documents.
Args:
documents: List of MongoDB documents
Returns:
Dictionary containing schema information
"""
if not documents:
return {"fields": [], "field_types": {}}
# Collect all field names and types
field_types = {}
for doc in documents:
for key, value in doc.items():
if key not in field_types:
field_types[key] = set()
field_types[key].add(type(value).__name__)
# Convert sets to lists
schema = {
"fields": list(field_types.keys()),
"field_types": {
k: list(v) if len(v) > 1 else list(v)[0]
for k, v in field_types.items()
},
}
return schema
+341
View File
@@ -0,0 +1,341 @@
"""
Pandas Ingestion Module
This module provides comprehensive pandas DataFrame ingestion capabilities for the
Semantica framework, enabling data extraction from DataFrames, CSV, JSON, and dictionaries.
Key Features:
- DataFrame ingestion from various sources
- CSV, JSON, dictionary conversion
- Data transformation and validation
- Schema extraction
- Large dataset handling with chunking
Main Classes:
- PandasIngestor: Main pandas ingestion class
- PandasData: Data representation for pandas ingestion
Example Usage:
>>> from semantica.ingest import PandasIngestor
>>> import pandas as pd
>>> ingestor = PandasIngestor()
>>> df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [25, 30]})
>>> data = ingestor.ingest_dataframe(df)
>>> csv_data = ingestor.from_csv("data.csv")
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
try:
import pandas as pd
except ImportError:
pd = None
@dataclass
class PandasData:
"""Pandas data representation."""
dataframe: Any # pd.DataFrame
row_count: int
column_count: int
columns: List[str]
dtypes: Dict[str, str]
schema: Dict[str, Any]
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class PandasIngestor:
"""
Pandas ingestion handler.
This class provides comprehensive pandas DataFrame ingestion capabilities,
supporting ingestion from DataFrames, CSV files, JSON files, and dictionaries.
Features:
- DataFrame ingestion
- CSV file reading
- JSON file reading
- Dictionary conversion
- Schema extraction
- Data validation
- Large dataset chunking
Example Usage:
>>> ingestor = PandasIngestor()
>>> data = ingestor.ingest_dataframe(df)
>>> csv_data = ingestor.from_csv("data.csv")
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize pandas ingestor.
Args:
config: Optional pandas ingestion configuration dictionary
**kwargs: Additional configuration parameters (merged into config)
"""
if pd is None:
raise ImportError(
"pandas is required for PandasIngestor. Install it with: pip install pandas"
)
self.logger = get_logger("pandas_ingestor")
self.config = config or {}
self.config.update(kwargs)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
self.logger.debug("Pandas ingestor initialized")
def ingest_dataframe(
self, dataframe: Any, **options
) -> PandasData:
"""
Ingest data from pandas DataFrame.
This method processes a pandas DataFrame and extracts metadata,
schema information, and data statistics.
Args:
dataframe: pandas DataFrame to ingest
**options: Additional processing options
Returns:
PandasData: Ingested data object containing:
- dataframe: Original DataFrame
- row_count: Number of rows
- column_count: Number of columns
- columns: List of column names
- dtypes: Dictionary mapping column names to data types
- schema: Schema information dictionary
- metadata: Additional metadata
Raises:
ValidationError: If dataframe is not a valid pandas DataFrame
ProcessingError: If ingestion fails
"""
if not isinstance(dataframe, pd.DataFrame):
raise ValidationError(
f"Expected pandas DataFrame, got {type(dataframe).__name__}"
)
tracking_id = self.progress_tracker.start_tracking(
file="dataframe",
module="ingest",
submodule="PandasIngestor",
message="Ingesting pandas DataFrame",
)
try:
# Extract basic information
row_count = len(dataframe)
column_count = len(dataframe.columns)
columns = list(dataframe.columns)
dtypes = {col: str(dtype) for col, dtype in dataframe.dtypes.items()}
# Build schema information
schema = {
"columns": columns,
"dtypes": dtypes,
"shape": (row_count, column_count),
"index_type": str(type(dataframe.index).__name__),
"has_nulls": dataframe.isnull().any().any(),
"null_counts": dataframe.isnull().sum().to_dict(),
}
# Extract metadata
metadata = {
"memory_usage": dataframe.memory_usage(deep=True).sum(),
"index_name": dataframe.index.name,
}
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested DataFrame: {row_count} rows, {column_count} columns",
)
self.logger.info(
f"DataFrame ingestion completed: {row_count} row(s), {column_count} column(s)"
)
return PandasData(
dataframe=dataframe,
row_count=row_count,
column_count=column_count,
columns=columns,
dtypes=dtypes,
schema=schema,
metadata=metadata,
)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest DataFrame: {e}")
raise ProcessingError(f"Failed to ingest DataFrame: {e}") from e
def from_csv(
self,
file_path: Union[str, Path],
**pandas_options,
) -> PandasData:
"""
Ingest data from CSV file.
This method reads a CSV file using pandas and ingests it as a DataFrame.
Args:
file_path: Path to CSV file
**pandas_options: Additional options passed to pd.read_csv()
Returns:
PandasData: Ingested data object
Raises:
ProcessingError: If CSV reading fails
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(f"CSV file not found: {file_path}")
tracking_id = self.progress_tracker.start_tracking(
file=str(file_path),
module="ingest",
submodule="PandasIngestor",
message=f"Ingesting CSV: {file_path.name}",
)
try:
# Read CSV with pandas
dataframe = pd.read_csv(file_path, **pandas_options)
self.progress_tracker.update_tracking(
tracking_id, message="CSV read successfully, processing DataFrame..."
)
# Ingest the DataFrame
return self.ingest_dataframe(dataframe, **pandas_options)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest CSV {file_path}: {e}")
raise ProcessingError(f"Failed to ingest CSV: {e}") from e
def from_json(
self,
file_path: Union[str, Path],
**pandas_options,
) -> PandasData:
"""
Ingest data from JSON file.
This method reads a JSON file using pandas and ingests it as a DataFrame.
Args:
file_path: Path to JSON file
**pandas_options: Additional options passed to pd.read_json()
Returns:
PandasData: Ingested data object
Raises:
ProcessingError: If JSON reading fails
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(f"JSON file not found: {file_path}")
tracking_id = self.progress_tracker.start_tracking(
file=str(file_path),
module="ingest",
submodule="PandasIngestor",
message=f"Ingesting JSON: {file_path.name}",
)
try:
# Read JSON with pandas
dataframe = pd.read_json(file_path, **pandas_options)
self.progress_tracker.update_tracking(
tracking_id, message="JSON read successfully, processing DataFrame..."
)
# Ingest the DataFrame
return self.ingest_dataframe(dataframe, **pandas_options)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest JSON {file_path}: {e}")
raise ProcessingError(f"Failed to ingest JSON: {e}") from e
def from_dict(
self,
data: Union[Dict[str, List], List[Dict[str, Any]]],
**pandas_options,
) -> PandasData:
"""
Ingest data from dictionary.
This method converts a dictionary or list of dictionaries to a DataFrame
and ingests it.
Args:
data: Dictionary or list of dictionaries
**pandas_options: Additional options passed to pd.DataFrame()
Returns:
PandasData: Ingested data object
Raises:
ValidationError: If data format is invalid
ProcessingError: If conversion fails
"""
if not isinstance(data, (dict, list)):
raise ValidationError(
f"Expected dict or list, got {type(data).__name__}"
)
tracking_id = self.progress_tracker.start_tracking(
file="dictionary",
module="ingest",
submodule="PandasIngestor",
message="Converting dictionary to DataFrame",
)
try:
# Convert to DataFrame
dataframe = pd.DataFrame(data, **pandas_options)
self.progress_tracker.update_tracking(
tracking_id, message="Dictionary converted, processing DataFrame..."
)
# Ingest the DataFrame
return self.ingest_dataframe(dataframe, **pandas_options)
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
self.logger.error(f"Failed to ingest dictionary: {e}")
raise ProcessingError(f"Failed to ingest dictionary: {e}") from e