File size: 4,680 Bytes
abcd0c2
 
 
 
 
 
 
c5638b0
 
abcd0c2
 
 
 
 
 
 
 
 
 
 
c5638b0
 
 
 
 
abcd0c2
c5638b0
 
 
 
 
abcd0c2
 
 
 
 
 
 
 
 
c5638b0
 
 
abcd0c2
 
 
 
c5638b0
 
 
 
 
 
 
 
 
 
 
 
 
abcd0c2
 
c5638b0
abcd0c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5638b0
abcd0c2
 
c5638b0
abcd0c2
c5638b0
 
 
 
 
 
 
abcd0c2
 
 
 
c5638b0
abcd0c2
c5638b0
abcd0c2
c5638b0
abcd0c2
 
 
c5638b0
abcd0c2
c5638b0
abcd0c2
 
c5638b0
 
 
 
 
 
 
abcd0c2
c5638b0
 
 
abcd0c2
c5638b0
 
 
 
 
 
 
 
 
 
 
abcd0c2
 
 
c5638b0
 
abcd0c2
 
c5638b0
 
abcd0c2
c5638b0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
JSON extractor for structured data files.

Supports only CSV, XLS, XLSX file types for JSON extraction.
Returns error for unsupported file types.
"""

from __future__ import annotations

import io
import warnings
from pathlib import Path
from typing import Any, Dict, Optional, Union

import pandas as pd

from logger import get_logger

logger = get_logger(__name__)

SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".csv", ".xls", ".xlsx"})

MAX_CSV_ROWS = 100_000
MAX_EXCEL_ROWS = 50_000
MAX_CELL_COUNT = 2_000_000

try:
    from app.core.config import settings as _app_settings
    MAX_FILE_SIZE_BYTES: int = _app_settings.MAX_FILE_SIZE_BYTES
except Exception:
    MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024


def _validate_file_size(size: int) -> Optional[str]:
    if size > MAX_FILE_SIZE_BYTES:
        return f"File size {size} bytes exceeds limit of {MAX_FILE_SIZE_BYTES} bytes"
    return None


def _check_memory_usage(rows: int, cols: int) -> Optional[str]:
    cell_count = rows * cols
    if cell_count > MAX_CELL_COUNT:
        approx_mb = (cell_count * 50) / (1024 * 1024)
        return f"Data size too large (approx {approx_mb:.1f} MB). Too many cells: {rows}x{cols}"
    return None


def _read_dataframe(ext: str, file_path: Union[str, Path], file_data: Optional[bytes]) -> "pd.DataFrame":
    """Read a CSV/Excel file from either an in-memory buffer or a path."""
    if ext == ".csv":
        if file_data:
            return pd.read_csv(io.BytesIO(file_data), nrows=MAX_CSV_ROWS + 1, low_memory=False)
        return pd.read_csv(file_path, nrows=MAX_CSV_ROWS + 1, low_memory=False)

    engine = "openpyxl" if ext == ".xlsx" else "xlrd"
    if file_data:
        return pd.read_excel(io.BytesIO(file_data), engine=engine)
    return pd.read_excel(file_path, engine=engine)


def extract_json_from_file(
    file_path: Union[str, Path],
    file_data: Optional[bytes] = None,
) -> Dict[str, Any]:
    """
    Extract JSON data from structured files (CSV, XLS, XLSX).

    Parameters
    ----------
    file_path : Union[str, Path]
        Path to the file or filename with extension
    file_data : Optional[bytes]
        Raw file data (for stream processing)

    Returns
    -------
    Dict[str, Any]
        Extracted data or error information
    """
    ext = Path(file_path).suffix.lower()

    if ext not in SUPPORTED_EXTENSIONS:
        return {
            "error": f"Unsupported file type: {ext}. Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}",
            "file_type": ext,
        }

    # Validate file size
    if file_data is not None:
        source_bytes = len(file_data)
    else:
        path = Path(file_path)
        source_bytes = path.stat().st_size if path.exists() else 0
    size_error = _validate_file_size(source_bytes)
    if size_error:
        return {"error": size_error, "file_type": ext}

    try:
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", UserWarning)
            df = _read_dataframe(ext, file_path, file_data)
    except pd.errors.EmptyDataError:
        return {"error": "File is empty or has no data", "file_type": ext}
    except MemoryError:
        return {"error": "Out of memory processing file", "file_type": ext}
    except Exception as exc:
        logger.exception("JSON extraction failed for %s", ext)
        return {
            "error": f"Processing failed: {exc}",
            "file_type": ext,
            "exception_type": type(exc).__name__,
        }

    max_rows = MAX_EXCEL_ROWS if ext != ".csv" else MAX_CSV_ROWS
    if len(df) > max_rows:
        return {
            "error": f"File contains {len(df)} rows, exceeds limit of {max_rows}",
            "file_type": ext,
            "row_count": len(df),
        }

    mem_error = _check_memory_usage(len(df), len(df.columns))
    if mem_error:
        return {"error": mem_error, "file_type": ext}

    logger.info("Extracted JSON from %s: %d rows, %d cols", ext, len(df), len(df.columns))
    return {
        "success": True,
        "file_type": ext,
        "data": {
            "columns": list(df.columns),
            "rows": df.where(pd.notnull(df), None).to_dict(orient="records"),
            "shape": [len(df), len(df.columns)],
            "dtypes": {col: str(dtype) for col, dtype in df.dtypes.items()},
        },
    }


def is_supported_file_type(file_path: Union[str, Path]) -> bool:
    """Check if file type is supported for JSON extraction."""
    return Path(file_path).suffix.lower() in SUPPORTED_EXTENSIONS


def get_supported_extensions() -> list[str]:
    """Get sorted list of supported file extensions for JSON extraction."""
    return sorted(SUPPORTED_EXTENSIONS)