afb6d5185d
Feature/lesser table caching refactor hybrid * chore: Remove unused duplicate main.py from shared pipeline * fix: Correct crosswalk paths in aarete_derived.py * chore: Remove unused documentation files from fieldExtraction * docs: Add documentation files to documentation folder * docs: Update README with uv setup, expanded project structure, and branching conventions * docs: Add uv installation steps with Ubuntu/WSL emphasis * Enable prompt caching for all remaining LLM calls - Add _INSTRUCTION() functions for: EXHIBIT_HEADER, EXHIBIT_LINKAGE, EXHIBIT_TITLE_MATCH, DATE_FIX, DERIVED_TERM_DATE, CHECK_PROVIDER_NAME_MATCH, SPECIAL_CASE_ASSIGNMENT - Update all invoke_claude() calls in saas and clover pipelines to use cache=True with corresponding _INSTRUCTION() functions - Add new instructions to get_cacheable_instructions() for cache warming - Update tests for new instruction functions Functions now using caching: - prompt_exhibit_level - prompt_exhibit_lesser (EXHIBIT_LEVEL_LESSER_OF) - prompt_fee_schedule_breakout - prompt_grouper_breakout - prompt_special_case_assignment - prompt_exhibit_linkage - prompt_exhibit_header - prompt_smart_chunked (ONE_TO_ONE templates) - prompt_date_fix - prompt_derived_term_date - prompt_exhibit_title_match - provider_name_match_check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Reorder * feat: Add bcbs_promise client pipeline with OFFSET_TERM extraction - Add new bcbs_promise client with HSC-based OFFSET_TERM field extraction - Extract full paragraph text of offset/recoupment provisions from contracts - Derive OFFSET_INDICATOR (Y/N) from OFFSET_TERM presence - Fix reorder_columns to preserve extra columns not in COLUMN_ORDER - Update QC/QA output path to outputs/qc_qa/ * fix: Update dev deps and test assertions for QC/QA output path - Add pytest/pytest-mock to dev dependencies for mypy type checking - Update test assertions to expect outputs/qc_qa instead of qa_qc_output * style: Apply black formatting to prompt_templates.py * Merge main, move scripts * Archive some scripts * update py version * remove .py version file * Remove ASCII characters * Restore testbed code * restore tracking * Update testbed metrics * Enable prompt caching for CODE_LAST_CHECK, FILL_BILL_TYPE, DUAL_LOB_CHECK, and GROUPER_BREAKOUT - Add CODE_LAST_CHECK_INSTRUCTION() for service specificity classification - Add FILL_BILL_TYPE_INSTRUCTION() for bill type code determination - Add DUAL_LOB_CHECK_INSTRUCTION() for Medicare/Medicaid classification - Update code_funcs.py to use caching for CODE_LAST_CHECK, FILL_BILL_TYPE, GROUPER_BREAKOUT - Update postprocessing_funcs.py to use caching for DUAL_LOB_CHECK - Add new instructions to get_cacheable_instructions() for cache warming - Add unit tests for new instruction functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix postprocessing_funcs to remove invalid columns * Merge branch 'main' into feature/lesser-table-caching-refactor-hybrid * Revert prompt caching changes from aed1b73c * update formatting * Update imports Approved-by: Sha Brown Approved-by: Praneel Panchigar
308 lines
11 KiB
Python
308 lines
11 KiB
Python
import csv
|
|
import boto3
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
import os
|
|
|
|
"""
|
|
This is an adhoc script that is used tot copy files from one prefix to another prefix within the same S3 bucket.
|
|
|
|
TYPICALLY USED FOR CENTENE-HEALTHNET
|
|
|
|
The need for this script is when the client team requests re-run and the previous runs are stored in a different prefixes.
|
|
|
|
The script reads a CSV file containing 'File Name' and 'Type' columns. It then searches for these files in the specified prefixes
|
|
and copies them to a new destination prefix within the same bucket.
|
|
|
|
The script also writes a CSV file containing the files that were not found in the specified prefixes.
|
|
|
|
The script is multithreaded to improve performance when searching for files in multiple prefixes.
|
|
|
|
This script can be used to just location files by commenting out the copy functionality.
|
|
|
|
Usage:
|
|
1. Update the configuration section below with the appropriate values.
|
|
2. Run the script using Python 3.
|
|
|
|
Note:
|
|
- Ensure that the CSV file contains 'File Name' and 'Type' columns.
|
|
- Ensure you have the required permissions to read from and write to the specified S3 bucket.
|
|
|
|
"""
|
|
|
|
# --------------------- Configuration ---------------------
|
|
|
|
# Path to your CSV file from client / DS team that need to be staged
|
|
CSV_FILE = 'C:\\Doczy\\Health net\\no_tin_filenames.csv' # Ensure this file has 'File Name' and 'Type' columns
|
|
# AWS Configuration
|
|
PROFILE_NAME = 'doczy_uat' # Replace with your AWS profile name
|
|
BUCKET_NAME = 'centene-healthnet-files' # Replace with your S3 bucket name
|
|
|
|
# List of prefixes to search within the bucket
|
|
PREFIXES = [
|
|
'facility_rerun_092524/txt_files/',
|
|
'facility-remaining-text-files/',
|
|
'agreements/text_files/',
|
|
'ammendments/text_files/',
|
|
'1kfacilityrun/',
|
|
'ancillary_1200_rerun/txt_files/',
|
|
'facility_remaining_1k_rerun/',
|
|
'facility-table-issue-files/',
|
|
'facility-test-files/',
|
|
'facility-textract-rerun/txt_files/',
|
|
'HN-10-Ancillary-Mix/',
|
|
'lessthan200pagesfiles/',
|
|
'new_prof_and_ancillary_1200/',
|
|
'professional_text_files_for_llm/',
|
|
'text_files_for_llm/',
|
|
'professional_paragraph_format_rerun/',
|
|
'HN-10-Ancillary-Mix/',
|
|
'facility_rerun_092524/',
|
|
'adhoc/'
|
|
# Add more prefixes as needed
|
|
]
|
|
|
|
# Destination prefix for copying files (within the same bucket)
|
|
DESTINATION_PREFIX = 'facility_rerun_112124/no_tin/' # Replace with your desired destination prefix
|
|
|
|
# Number of threads for multithreading
|
|
MAX_WORKERS = 100
|
|
|
|
# Output CSV for files not found
|
|
NOT_FOUND_CSV = 'C:\\Doczy\\Health net\\files_not_found_no_tin.csv'
|
|
|
|
|
|
# --------------------- Functions ---------------------
|
|
|
|
def create_s3_client(profile_name):
|
|
"""
|
|
Create a Boto3 S3 client using the specified AWS profile.
|
|
"""
|
|
session = boto3.Session(profile_name=profile_name)
|
|
s3_client = session.client('s3')
|
|
return s3_client
|
|
|
|
def normalize_string(s):
|
|
"""
|
|
Normalize a string by removing spaces and converting to lowercase.
|
|
|
|
Parameters:
|
|
s (str): The string to normalize.
|
|
|
|
Returns:
|
|
str: The normalized string.
|
|
"""
|
|
return ''.join(s.split()).lower()
|
|
|
|
def read_csv(csv_file):
|
|
"""
|
|
Read the CSV file and extract the list of file names along with their types.
|
|
|
|
Parameters:
|
|
csv_file (str): Path to the CSV file.
|
|
|
|
Returns:
|
|
list of dict: List containing dictionaries with 'File Name' and 'Type'.
|
|
"""
|
|
files = []
|
|
try:
|
|
with open(csv_file, mode='r', newline='', encoding='utf-8') as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
files.append({
|
|
'File Name': row['File Name'],
|
|
'Type': row['Type']
|
|
})
|
|
except FileNotFoundError:
|
|
print(f"Error: The file {csv_file} does not exist.")
|
|
except KeyError:
|
|
print("Error: CSV file must contain 'File Name' and 'Type' columns.")
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred while reading the CSV: {e}")
|
|
return files
|
|
|
|
def list_objects_in_prefix(s3_client, bucket, prefix):
|
|
"""
|
|
List all objects in a given prefix and return a mapping of normalized keys to actual keys.
|
|
|
|
Parameters:
|
|
s3_client (boto3.client): Boto3 S3 client.
|
|
bucket (str): S3 bucket name.
|
|
prefix (str): Prefix to list objects from.
|
|
|
|
Returns:
|
|
dict: Mapping of normalized keys to actual S3 keys.
|
|
"""
|
|
normalized_key_mapping = {}
|
|
continuation_token = None
|
|
while True:
|
|
try:
|
|
if continuation_token:
|
|
response = s3_client.list_objects_v2(
|
|
Bucket=bucket,
|
|
Prefix=prefix,
|
|
ContinuationToken=continuation_token
|
|
)
|
|
else:
|
|
response = s3_client.list_objects_v2(
|
|
Bucket=bucket,
|
|
Prefix=prefix
|
|
)
|
|
if 'Contents' in response:
|
|
for obj in response['Contents']:
|
|
actual_key = obj['Key']
|
|
normalized_key = normalize_string(os.path.basename(actual_key).replace('.txt', ''))
|
|
normalized_key_mapping[normalized_key] = actual_key
|
|
if response.get('IsTruncated'):
|
|
continuation_token = response.get('NextContinuationToken')
|
|
else:
|
|
break
|
|
except Exception as e:
|
|
print(f"Error listing objects in prefix {prefix}: {e}")
|
|
break
|
|
return normalized_key_mapping
|
|
|
|
def build_normalized_key_mapping(s3_client, bucket, prefixes):
|
|
"""
|
|
Build a mapping from normalized filenames to actual S3 keys across all specified prefixes.
|
|
|
|
Parameters:
|
|
s3_client (boto3.client): Boto3 S3 client.
|
|
bucket (str): S3 bucket name.
|
|
prefixes (list): List of prefixes to search.
|
|
|
|
Returns:
|
|
dict: Mapping of normalized filenames to actual S3 keys.
|
|
"""
|
|
normalized_key_mapping = {}
|
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
|
future_to_prefix = {
|
|
executor.submit(list_objects_in_prefix, s3_client, bucket, prefix): prefix
|
|
for prefix in prefixes
|
|
}
|
|
for future in as_completed(future_to_prefix):
|
|
prefix = future_to_prefix[future]
|
|
try:
|
|
prefix_mapping = future.result()
|
|
# Handle potential duplicate normalized keys
|
|
for norm_key, actual_key in prefix_mapping.items():
|
|
if norm_key in normalized_key_mapping:
|
|
# This case is has never been hit in the past
|
|
# It is possible that the same file name is present in multiple prefixes (For Centene National Contracting)
|
|
# print(f"Warning: Duplicate normalized key '{norm_key}' found in prefix '{prefix}'.")
|
|
pass
|
|
else:
|
|
normalized_key_mapping[norm_key] = actual_key
|
|
except Exception as exc:
|
|
print(f"Prefix {prefix} generated an exception: {exc}")
|
|
return normalized_key_mapping
|
|
|
|
def copy_file(s3_client, bucket, source_key, destination_key):
|
|
"""
|
|
Copy a file from source_key to destination_key within the same bucket.
|
|
|
|
Parameters:
|
|
s3_client (boto3.client): Boto3 S3 client.
|
|
bucket (str): S3 bucket name.
|
|
source_key (str): Source S3 key.
|
|
destination_key (str): Destination S3 key.
|
|
"""
|
|
copy_source = {'Bucket': bucket, 'Key': source_key}
|
|
try:
|
|
s3_client.copy(copy_source, bucket, destination_key)
|
|
print(f"Copied {source_key} to {destination_key}")
|
|
except Exception as e:
|
|
print(f"Failed to copy {source_key} to {destination_key}: {e}")
|
|
|
|
def write_not_found_csv(not_found_files, output_csv):
|
|
"""
|
|
Write the list of not found files to a CSV file.
|
|
|
|
Parameters:
|
|
not_found_files (list of dict): List containing dictionaries with 'File Name' and 'Type'.
|
|
output_csv (str): Path to the output CSV file.
|
|
"""
|
|
try:
|
|
with open(output_csv, mode='w', newline='', encoding='utf-8') as f:
|
|
writer = csv.DictWriter(f, fieldnames=['File Name', 'Type'])
|
|
writer.writeheader()
|
|
for file in not_found_files:
|
|
writer.writerow(file)
|
|
print(f"Successfully wrote {len(not_found_files)} not found files to {output_csv}")
|
|
except Exception as e:
|
|
print(f"Failed to write not found files to CSV: {e}")
|
|
|
|
def main():
|
|
# Create S3 client
|
|
s3_client = create_s3_client(PROFILE_NAME)
|
|
|
|
# Read filenames and types from CSV
|
|
files = read_csv(CSV_FILE)
|
|
if not files:
|
|
print("No files to process. Exiting.")
|
|
return
|
|
print(f"Total files to search: {len(files)}")
|
|
|
|
# Normalize filenames and build a mapping from normalized name to original file info
|
|
normalized_files_mapping = {}
|
|
for file in files:
|
|
original_name = file['File Name']
|
|
normalized_name = normalize_string(original_name) + '.txt' # Append '.txt' as per original logic
|
|
normalized_files_mapping[normalized_name] = file
|
|
|
|
# Build normalized key mapping from S3
|
|
print("Listing and mapping S3 objects...")
|
|
normalized_key_mapping = build_normalized_key_mapping(s3_client, BUCKET_NAME, PREFIXES)
|
|
print(f"Total S3 objects mapped: {len(normalized_key_mapping)}")
|
|
|
|
# Match normalized filenames with normalized S3 keys
|
|
found_files = {} # Mapping of original file info to actual S3 keys
|
|
for norm_filename, file_info in normalized_files_mapping.items():
|
|
norm_key = normalize_string(norm_filename.replace('.txt', '')) # Normalize without '.txt'
|
|
actual_key = normalized_key_mapping.get(norm_key)
|
|
if actual_key:
|
|
found_files[file_info['File Name']] = actual_key
|
|
else:
|
|
# File not found; will be handled later
|
|
pass
|
|
|
|
# Display the results
|
|
total_found = len(found_files)
|
|
print(f"\nTotal files found: {total_found}")
|
|
for fname, key in found_files.items():
|
|
print(f"Found: {key}")
|
|
|
|
# Identify files not found
|
|
found_filenames = set(found_files.keys())
|
|
not_found_files = [file for file in files if file['File Name'] not in found_filenames]
|
|
|
|
# Write not found files to CSV
|
|
if not_found_files:
|
|
write_not_found_csv(not_found_files, NOT_FOUND_CSV)
|
|
else:
|
|
print("All files were found. No entries to write to not_found CSV.")
|
|
|
|
# ----------------- Copy Functionality -----------------
|
|
|
|
print("\nStarting to copy files to the destination prefix...")
|
|
|
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as copy_executor:
|
|
copy_futures = []
|
|
for filename, source_key in found_files.items():
|
|
destination_key = os.path.join(DESTINATION_PREFIX, os.path.basename(source_key))
|
|
copy_futures.append(
|
|
copy_executor.submit(copy_file, s3_client, BUCKET_NAME, source_key, destination_key)
|
|
)
|
|
|
|
# Optionally, wait for all copy operations to complete
|
|
for future in as_completed(copy_futures):
|
|
try:
|
|
future.result()
|
|
except Exception as exc:
|
|
print(f"Copy operation generated an exception: {exc}")
|
|
|
|
print("File copying completed.")
|
|
|
|
# ----------------------------------------------------------
|
|
|
|
if __name__ == '__main__':
|
|
main() |