- Remove DiscreteMaterialPlanExtractor class (stateful) - Replace with pure functions: chunk_order_ids, get_login_url, extract_batch, etc. - All functions are stateless, accept explicit parameters - Caller manages browser/session lifecycle (consistent with extractor_core.py) - Lower coupling: no direct dependency on utils.auth.login - Update tests to match new function signatures Breaking Changes: - DiscreteMaterialPlanExtractor class removed - Use extract_and_post_process() or extract_from_file() instead of class methods - Caller must manage browser session before calling extractor functions
335 lines
9.8 KiB
Python
335 lines
9.8 KiB
Python
"""
|
|
Discrete Material Plan Data Extractor
|
|
|
|
Pure functions for extracting and post-processing discrete material plan data.
|
|
All functions are stateless and accept required parameters explicitly.
|
|
Caller is responsible for browser/session lifecycle management.
|
|
"""
|
|
|
|
import pandas as pd
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
from playwright.sync_api import Page, Frame
|
|
|
|
|
|
def chunk_order_ids(order_ids: List[str], batch_size: int) -> List[List[str]]:
|
|
"""
|
|
Split order IDs into batches.
|
|
|
|
Args:
|
|
order_ids: List of order IDs to process
|
|
batch_size: Maximum number of order IDs per batch
|
|
|
|
Returns:
|
|
List of batches, where each batch is a list of order IDs
|
|
|
|
Example:
|
|
>>> chunk_order_ids(["A", "B", "C", "D"], 2)
|
|
[["A", "B"], ["C", "D"]]
|
|
"""
|
|
return [
|
|
order_ids[i : i + batch_size]
|
|
for i in range(0, len(order_ids), batch_size)
|
|
]
|
|
|
|
|
|
def get_login_url(base_url: str) -> str:
|
|
"""
|
|
Construct the complete login URL from base URL.
|
|
|
|
Args:
|
|
base_url: Base ERP URL (e.g., "https://erp.example.com")
|
|
|
|
Returns:
|
|
Complete login page URL
|
|
|
|
Example:
|
|
>>> get_login_url("https://erp.example.com")
|
|
"https://erp.example.com/yonbip/resources/uap/rbac/login/main/index.html"
|
|
"""
|
|
return f"{base_url.rstrip('/')}/yonbip/resources/uap/rbac/login/main/index.html"
|
|
|
|
|
|
def extract_batch(
|
|
work_frame: Frame,
|
|
page: Page,
|
|
order_ids: List[str],
|
|
batch_index: int,
|
|
download_dir: str,
|
|
) -> str:
|
|
"""
|
|
Execute download workflow for a single batch of order IDs.
|
|
|
|
This is a thin wrapper around `execute_batch_download_workflow` from
|
|
extractor_core.py, providing progress reporting context.
|
|
|
|
Args:
|
|
work_frame: The work iframe containing the search form and data grid
|
|
page: The Playwright page object for download handling
|
|
order_ids: List of order IDs for this batch
|
|
batch_index: Zero-based batch index for naming the output file
|
|
download_dir: Directory path to save the downloaded file
|
|
|
|
Returns:
|
|
Full path to the downloaded Excel file
|
|
"""
|
|
from .extractor_core import execute_batch_download_workflow
|
|
|
|
return execute_batch_download_workflow(
|
|
work_frame=work_frame,
|
|
page=page,
|
|
order_ids=order_ids,
|
|
batch_index=batch_index,
|
|
download_dir=download_dir,
|
|
)
|
|
|
|
|
|
def extract_batches(
|
|
work_frame: Frame,
|
|
page: Page,
|
|
order_ids: List[str],
|
|
download_dir: str,
|
|
batch_size: int = 10,
|
|
) -> List[str]:
|
|
"""
|
|
Download data for multiple batches of order IDs.
|
|
|
|
Caller is responsible for:
|
|
- Browser session management (login, logout)
|
|
- Navigation to discrete material plan page
|
|
- Query interface setup
|
|
|
|
Args:
|
|
work_frame: The work iframe containing the data grid
|
|
page: The Playwright page object for download handling
|
|
order_ids: List of order IDs to download
|
|
download_dir: Directory path to save downloaded files
|
|
batch_size: Maximum number of order IDs per batch
|
|
|
|
Returns:
|
|
List of paths to downloaded Excel files
|
|
|
|
Example:
|
|
>>> # Caller manages session
|
|
>>> browser, context, page, main_frame = login(...)
|
|
>>> work_frame, page1 = navigate_to_discrete_material_page(main_frame, page)
|
|
>>> setup_query_interface(work_frame)
|
|
>>> files = extract_batches(work_frame, page1, order_ids, "/downloads")
|
|
>>> context.close()
|
|
>>> browser.close()
|
|
"""
|
|
from .extractor_core import setup_query_interface
|
|
|
|
downloaded_files = []
|
|
chunks = chunk_order_ids(order_ids, batch_size)
|
|
|
|
# Setup query interface once
|
|
setup_query_interface(work_frame)
|
|
|
|
# Process each batch
|
|
for batch_index, batch in enumerate(chunks):
|
|
file_path = extract_batch(
|
|
work_frame=work_frame,
|
|
page=page,
|
|
order_ids=batch,
|
|
batch_index=batch_index,
|
|
download_dir=download_dir,
|
|
)
|
|
downloaded_files.append(file_path)
|
|
|
|
return downloaded_files
|
|
|
|
|
|
def post_process_downloads(
|
|
downloaded_files: List[str],
|
|
output_file: str,
|
|
verbose: bool = True,
|
|
) -> Tuple[str, pd.DataFrame]:
|
|
"""
|
|
Convert and merge downloaded Excel files into structured DataFrame.
|
|
|
|
Uses ExcelConverter to convert each file, then merges all results.
|
|
|
|
Args:
|
|
downloaded_files: List of paths to downloaded Excel files
|
|
output_file: Path to save merged Excel result
|
|
verbose: Whether to print progress messages
|
|
|
|
Returns:
|
|
Tuple of (output_file_path, merged_dataframe)
|
|
|
|
Example:
|
|
>>> output_path, df = post_process_downloads(
|
|
... downloaded_files=["batch_1.xlsx", "batch_2.xlsx"],
|
|
... output_file="merged.xlsx",
|
|
... verbose=True
|
|
... )
|
|
"""
|
|
from .excel_converter import ExcelConverter
|
|
|
|
converter = ExcelConverter(verbose=verbose)
|
|
all_dfs = []
|
|
|
|
# Convert each file
|
|
for i, file_path in enumerate(downloaded_files):
|
|
if verbose:
|
|
print(f"Converting file {i + 1}/{len(downloaded_files)}: {file_path}")
|
|
|
|
# Convert (do not save intermediate result)
|
|
df = converter.convert(input_file=file_path)
|
|
all_dfs.append(df)
|
|
|
|
# Merge all DataFrames
|
|
if not all_dfs:
|
|
merged_df = pd.DataFrame()
|
|
else:
|
|
merged_df = pd.concat(all_dfs, ignore_index=True)
|
|
|
|
# Save merged result
|
|
output_path = Path(output_file)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
merged_df.to_excel(output_path, index=False)
|
|
|
|
if verbose:
|
|
print(f"Merged result saved to: {output_path}")
|
|
print(f"Total rows: {len(merged_df)}")
|
|
|
|
return str(output_path), merged_df
|
|
|
|
|
|
def extract_and_post_process(
|
|
work_frame: Frame,
|
|
page: Page,
|
|
order_ids: List[str],
|
|
download_dir: str,
|
|
output_file: str,
|
|
batch_size: int = 10,
|
|
verbose: bool = True,
|
|
) -> Tuple[str, pd.DataFrame]:
|
|
"""
|
|
Complete extraction workflow: download batches + post-process to merged Excel.
|
|
|
|
This is a high-level convenience function that orchestrates the full workflow.
|
|
Caller is still responsible for browser session management.
|
|
|
|
Args:
|
|
work_frame: The work iframe containing the data grid
|
|
page: The Playwright page object for download handling
|
|
order_ids: List of order IDs to extract
|
|
download_dir: Directory for temporary batch files
|
|
output_file: Path for final merged Excel output
|
|
batch_size: Maximum order IDs per batch
|
|
verbose: Whether to print progress messages
|
|
|
|
Returns:
|
|
Tuple of (output_file_path, merged_dataframe)
|
|
|
|
Example:
|
|
>>> # Caller manages session
|
|
>>> browser, context, page, main_frame = login(...)
|
|
>>> work_frame, page1 = navigate_to_discrete_material_page(main_frame, page)
|
|
>>> output_path, df = extract_and_post_process(
|
|
... work_frame, page1, order_ids, "/downloads", "output.xlsx"
|
|
... )
|
|
>>> context.close()
|
|
>>> browser.close()
|
|
"""
|
|
# Step 1: Download all batches
|
|
if verbose:
|
|
print(f"Downloading {len(order_ids)} orders in batches of {batch_size}...")
|
|
|
|
downloaded_files = extract_batches(
|
|
work_frame=work_frame,
|
|
page=page,
|
|
order_ids=order_ids,
|
|
download_dir=download_dir,
|
|
batch_size=batch_size,
|
|
)
|
|
|
|
if verbose:
|
|
print(f"Downloaded {len(downloaded_files)} batch file(s)")
|
|
|
|
# Step 2: Post-process (convert + merge)
|
|
output_path, merged_df = post_process_downloads(
|
|
downloaded_files=downloaded_files,
|
|
output_file=output_file,
|
|
verbose=verbose,
|
|
)
|
|
|
|
return output_path, merged_df
|
|
|
|
|
|
def read_order_ids_from_file(id_file: str, encoding: str = "utf-8") -> List[str]:
|
|
"""
|
|
Read order IDs from a text file (one ID per line).
|
|
|
|
Args:
|
|
id_file: Path to file containing order IDs
|
|
encoding: File encoding (default: utf-8)
|
|
|
|
Returns:
|
|
List of order IDs (stripped, empty lines filtered)
|
|
|
|
Raises:
|
|
FileNotFoundError: If id_file doesn't exist
|
|
|
|
Example:
|
|
>>> order_ids = read_order_ids_from_file("orders.txt")
|
|
"""
|
|
id_path = Path(id_file)
|
|
if not id_path.exists():
|
|
raise FileNotFoundError(f"Order ID file not found: {id_file}")
|
|
|
|
with open(id_path, "r", encoding=encoding) as f:
|
|
return [line.strip() for line in f if line.strip()]
|
|
|
|
|
|
def extract_from_file(
|
|
id_file: str,
|
|
work_frame: Frame,
|
|
page: Page,
|
|
download_dir: str,
|
|
output_file: str,
|
|
batch_size: int = 10,
|
|
verbose: bool = True,
|
|
) -> Tuple[str, pd.DataFrame]:
|
|
"""
|
|
Extract data from order IDs in a file and post-process to merged Excel.
|
|
|
|
Convenience function that reads IDs from file and calls extract_and_post_process.
|
|
|
|
Args:
|
|
id_file: Path to file containing order IDs (one per line)
|
|
work_frame: The work iframe containing the data grid
|
|
page: The Playwright page object for download handling
|
|
download_dir: Directory for temporary batch files
|
|
output_file: Path for final merged Excel output
|
|
batch_size: Maximum order IDs per batch
|
|
verbose: Whether to print progress messages
|
|
|
|
Returns:
|
|
Tuple of (output_file_path, merged_dataframe)
|
|
|
|
Example:
|
|
>>> # Read order IDs
|
|
>>> order_ids = read_order_ids_from_file("orders.txt")
|
|
>>> # Extract and process
|
|
>>> output_path, df = extract_from_file(
|
|
... "orders.txt", work_frame, page, "/downloads", "output.xlsx"
|
|
... )
|
|
"""
|
|
order_ids = read_order_ids_from_file(id_file)
|
|
|
|
if verbose:
|
|
print(f"Loaded {len(order_ids)} order IDs from {id_file}")
|
|
|
|
return extract_and_post_process(
|
|
work_frame=work_frame,
|
|
page=page,
|
|
order_ids=order_ids,
|
|
download_dir=download_dir,
|
|
output_file=output_file,
|
|
batch_size=batch_size,
|
|
verbose=verbose,
|
|
)
|