Merged in feature/adhoc-ops-scripts (pull request #275)

DRAFT: Feature/adhoc ops scripts

* added adhoc ops scripts for pre-doczy work

* Added first draft of cost automation script

* Fixed dir name

* Added Scheduler lambda

* Added TX adhoc script for rerun + Updated cost automation script

* Added some misc scripts

* Merged main into feature/adhoc-ops-scripts

* Aryan Ad Hoc Scripts Pushed

* De Duplication Script added

* Merged main into feature/adhoc-ops-scripts


Approved-by: Umang Shailesh Mistry
This commit is contained in:
Michael McGuinness
2024-12-11 15:46:49 +00:00
committed by Umang Shailesh Mistry
parent e8d231b801
commit 6c028ef892
16 changed files with 5139 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first part of the process is to take a list of file names and their respective page counts and perform a simple de-duplication process that takes care of renaming files which are not actually duplicates and discarding those which are \"true\" duplicates. \n",
"* We decide this based on the page counts. If the same file names have different page counts, they are not actually duplicates. However if the file name is same and the page count is same - they are classified as \"true\" duplicates.\n",
"\n",
"The Input CSV should essentially have two important columns - All the File Names, File Paths, and their Page Counts.\n",
"\n",
"Our output for duplicate file names should rename them as (1), (2), .... and so on.\n",
"\n",
"Follow the comments to understand the step by step procedure taken to handle de-duplication and conduct renaming."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Importing pandas to deal with the csv as a dataframe\n",
"import pandas as pd\n",
"# Importing concurrent futures to support multithreading\n",
"import concurrent.futures\n",
"\n",
"# Helper Function that takes in a group of files with the exact same file name and performs the core logic\n",
"def process_file_group(group):\n",
" # This first drops rows with same file name and same page count and then sorts the page counts in order\n",
" group = group.drop_duplicates(subset=['File Name', 'Page Count']).sort_values('Page Count').reset_index(drop=True)\n",
" # This is just a simple function to add the index as (n) in the correct place of the file name\n",
" def add_suffix(file_name, idx):\n",
" if file_name.endswith('.Pdf'):\n",
" return file_name[:-4] + f'({idx + 1}).Pdf'\n",
" elif file_name.endswith('.pdf'):\n",
" return file_name[:-4] + f'({idx + 1}).pdf'\n",
" else:\n",
" return file_name + f'({idx + 1})'\n",
" # This run the above function on all of the file names in the group and populates those in a new column\n",
" group['New File Name'] = [add_suffix(file_name, idx) for idx, file_name in enumerate(group['File Name'])]\n",
" return group\n",
"\n",
"input_csv = 'icertis/icertis_files.csv'\n",
"df = pd.read_csv(input_csv)\n",
"# This is a good trick to identify rows which have duplicate file names and keep only those in the dataframe\n",
"duplicates_only = df[df.duplicated(subset=['File Name'], keep=False)]\n",
"# The group by function is used to bring all the same file names in the same group so that we can process and relabel those\n",
"grouped = duplicates_only.groupby(['File Name'])\n",
"print(grouped.head())\n",
"# This is a typical multithreading technique to run the process function on every group and store the results\n",
"with concurrent.futures.ThreadPoolExecutor() as executor:\n",
" results = executor.map(process_file_group, [group for _, group in grouped])\n",
"\n",
"# Now we simply put back the results into an output csv\n",
"optimized_df = pd.concat(results)\n",
"optimized_df.to_csv('icertis/icertis_duplicate_files.csv', index=False)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This the second step where we do two things - \n",
"* figure out which files we are concerned about in the current processing and get a csv of those original file names.\n",
"* Apply a custom 5-pager logic that does the following \n",
" * For a specific file name, if we have a file that has greater than 5 additional pages compared to all other duplicates of that file, we only take that max page count file\n",
" * If there are files within the 5 page range of the max page count file, we are not sure which file to consider and hence we add all those files\n",
"\n",
"We need to inputs here, one is the list of file names to consider and second is the previously generated csv of all duplicate files handled.\n",
"\n",
"Follow the comments to understand the step by step procedure taken"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Importing pandas to deal with the csv as a dataframe\n",
"import pandas as pd\n",
"\n",
"# Here we load the two input csvs and set up the output dataframe\n",
"df_dup = pd.read_csv('duplicate_files_list.csv')\n",
"# df_dup['File Name'] = df_dup['File Name'][:-4]\n",
"df_priority = pd.read_csv('batch6_16_comparison_output.csv')\n",
"df_priority = df_priority[df_priority['Only in Master Tracker'].str.contains(r'\\(\\d+\\)', na=False)]\n",
"# df_priority['File Name'] = df_priority['File Name'][:-4]\n",
"output_df = pd.DataFrame(columns=df_dup.columns)\n",
"\n",
"# Here we iterate over all the file names in the concerned file name list\n",
"print(len(df_priority['Only in Master Tracker']))\n",
"for file in df_priority['Only in Master Tracker']:\n",
" # We extract the relevant rows from the duplicate files that match the name\n",
" relevant_rows = df_dup[df_dup['New File Name'] == (file+\".Pdf\")]\n",
" # If there is only one matching row, we have no logic needed and can just use that\n",
" if len(relevant_rows) == 1:\n",
" output_df = pd.concat([output_df, relevant_rows])\n",
" # Otherwise we apply the 5 pager logic\n",
" elif len(relevant_rows) > 1:\n",
" # We use this max page count of all the matching files to compare against others\n",
" max_page_count = max(relevant_rows['Page Count'])\n",
" flag = False\n",
" for pgcnt in relevant_rows['Page Count'].unique():\n",
" # If we find another matching file within 5 pages of the max count, we can add that to our output\n",
" if max_page_count - pgcnt <= 5:\n",
" output_df = pd.concat([output_df, relevant_rows[relevant_rows['Page Count'] == pgcnt]])\n",
" else:\n",
" print(file)\n",
"\n",
"# here we do some post processing to ensure we are not keeping full duplicate rows and then convert the ouput df to a csv\n",
"output_df = output_df.drop_duplicates()\n",
"output_df.reset_index(drop=True, inplace=True)\n",
"output_df.to_csv('batch6_16_output_file_2.csv')\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+89
View File
@@ -0,0 +1,89 @@
import boto3
import csv
import os
import pandas as pd
def list_s3_files(bucket_name, folder):
s3 = boto3.Session(profile_name='temp_cred').client('s3')
paginator = s3.get_paginator('list_objects_v2')
operation_parameters = {'Bucket': bucket_name, 'Prefix': folder}
file_names = set()
for page in paginator.paginate(**operation_parameters):
if 'Contents' in page:
for content in page['Contents']:
file_name = content['Key']
if not file_name.endswith('/'):
file_name_without_extension = file_name.replace(folder, '', 1)[:-4]
# file_name_without_extension = file_name[:-4]
file_names.add(file_name_without_extension)
return file_names
def list_local_files(path):
files = set()
try:
for _, _, filenames in os.walk(path):
for filename in filenames:
files.add(filename[:-4])
except Exception as e:
print(f"Error accessing directory {path}: {e}")
return files
def compare_s3_folders(bucket_name, source1, folder1, source2, folder2, output_csv):
if source1 == 's3':
files_in_folder1 = list_s3_files(bucket_name, folder1)
elif source1 == 'local':
files_in_folder1 = list_local_files(folder1)
elif source1 == 'csv':
df = pd.read_csv(folder1,encoding='utf-8')
# df['File Name'] = df['File Name'][:-4]
files_in_folder1 = set(df['File Name'].to_list())
elif source1 == 'xlsb':
with pd.ExcelFile(folder1, engine='pyxlsb') as xlsb:
first_sheet = xlsb.sheet_names[0]
df = xlsb.parse(first_sheet)
files_in_folder1 = set(df['Contract Name'].to_list())
if source2 == 's3':
files_in_folder2 = list_s3_files(bucket_name, folder2)
elif source2 == 'local':
files_in_folder2 = list_local_files(folder2)
elif source2 == 'csv':
df = pd.read_csv(folder2,encoding='utf-8')
# df['File Name'] = df['File Name'][:-4]
files_in_folder2 = set(df['File Name'].to_list())
elif source2 == 'xlsb':
with pd.ExcelFile(folder2, engine='pyxlsb') as xlsb:
first_sheet = xlsb.sheet_names[0]
df = xlsb.parse(first_sheet)
files_in_folder2 = set(df['Contract Name'].to_list())
common_files = files_in_folder1 & files_in_folder2
only_in_folder1 = files_in_folder1 - files_in_folder2
only_in_folder2 = files_in_folder2 - files_in_folder1
with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow(['Common Files', f'Only in {folder1}', f'Only in {folder2}'])
max_length = max(len(common_files), len(only_in_folder1), len(only_in_folder2))
for i in range(max_length):
row = [
list(common_files)[i] if i < len(common_files) and list(common_files)[i] else '',
list(only_in_folder1)[i] if i < len(only_in_folder1) and list(only_in_folder1)[i] else '',
list(only_in_folder2)[i] if i < len(only_in_folder2) and list(only_in_folder2)[i] else ''
]
csv_writer.writerow(row)
bucket_name = 'centene-national-contracting-files'
source1_type = 'local' # local / s3 / csv / xlsb
source1 = 'Batch 6 TXT Files'
source2_type = 'csv' # local / s3 / csv / xlsb
source2 = 'Batch 6 Outputs'
output_csv = 'batch6/batch6_outputs_diff_with_tracker.csv'
compare_s3_folders(bucket_name, source1_type, source1, source2_type, source2, output_csv)
print(f'Comparison results have been written to {output_csv}')
+37
View File
@@ -0,0 +1,37 @@
import os
import csv
import glob
from PyPDF2 import PdfReader
from concurrent.futures import ThreadPoolExecutor, as_completed
def get_pdf_info(pdf_path):
try:
with open(pdf_path, 'rb') as file:
pdf_reader = PdfReader(file)
page_count = len(pdf_reader.pages)
except Exception as e:
print(f"Error reading {pdf_path}: {e}")
page_count = None
return os.path.basename(pdf_path), pdf_path, page_count
def collect_pdf_info(directory, output_csv):
pdf_files = glob.glob(os.path.join(directory, '**', '*.pdf'), recursive=True)
with open(output_csv, mode='w', newline='', encoding='utf-8') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(['File Name', 'File Path', 'Page Count'])
with ThreadPoolExecutor() as executor:
futures = {executor.submit(get_pdf_info, pdf): pdf for pdf in pdf_files}
for future in as_completed(futures):
result = future.result()
if result:
csv_writer.writerow(result)
print(f"CSV file '{output_csv}' created successfully.")
directory = 'T:/AArete Client Work/Doczy-Production/Restricted/icertis_contracts'
output_csv = 'icertis/icertis_files.csv'
collect_pdf_info(directory, output_csv)
@@ -0,0 +1,62 @@
import os
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
from collections import defaultdict
import csv
def build_file_cache(base_directory):
file_cache = defaultdict(list)
for root, dirs, files in os.walk(base_directory):
for file in files:
file_cache[file].append(os.path.join(root, file))
return file_cache
def find_files(file_cache, filenames):
found_files = {}
for filename in filenames:
if filename in file_cache:
found_files[filename] = file_cache[filename][0]
else:
found_files[filename] = "Not Found"
return found_files
def parallel_search(base_directory, filenames, max_workers=50):
print("Building file cache...")
file_cache = build_file_cache(base_directory)
with open('all_file_paths.csv', mode='w', newline='', encoding='utf-8') as csv_file:
writer = csv.writer(csv_file)
for key, values in file_cache.items():
row = [key] + values if isinstance(values, list) else [key, values]
writer.writerow(row)
print(f"Dictionary has been successfully written to 'all_file_paths.csv'.")
print(f"File cache built with {len(file_cache)} unique files.")
found_files = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(find_files, file_cache, chunk): chunk
for chunk in chunked(filenames, len(filenames) // max_workers)}
for future in as_completed(futures):
found_files.update(future.result())
return found_files
def chunked(iterable, n):
for i in range(0, len(iterable), n):
yield iterable[i:i + n]
def search_files_from_csv(csv_file, base_directory, output_csv):
df = pd.read_csv(csv_file)
df['filename'] += ".Pdf"
filenames = df['filename'].tolist()
print(f"Searching for {len(filenames)} files in '{base_directory}'...")
file_paths = parallel_search(base_directory, filenames)
df['file_path'] = df['filename'].apply(lambda x: file_paths.get(x, "Not Found"))
df.to_csv(output_csv, index=False)
print(f"Results saved to '{output_csv}'.")
input_csv = "missing_files_renaming.csv"
base_dir = "T:/AArete Client Work/Doczy-Production/Restricted/2024-06-28-pdf/"
output_csv = "missing_file_paths_2.csv"
search_files_from_csv(input_csv, base_dir, output_csv)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
import boto3
import concurrent.futures
s3 = boto3.Session(profile_name='temp_cred').client('s3')
def move_file(bucket_name, src_folder, dest_folder, file_key):
src_file_path = f"{src_folder}/{file_key}"
dest_file_path = f"{dest_folder}/{file_key}"
try:
s3.copy_object(
Bucket=bucket_name,
CopySource={'Bucket': bucket_name, 'Key': src_file_path},
Key=dest_file_path
)
s3.delete_object(Bucket=bucket_name, Key=src_file_path)
print(f"Moved {src_file_path} to {dest_file_path}")
except Exception as e:
print(f"Error moving {src_file_path}: {e}")
def list_files(bucket_name, folder, extension=None):
files = []
paginator = s3.get_paginator('list_objects_v2')
try:
for page in paginator.paginate(Bucket=bucket_name, Prefix=folder):
contents = page.get('Contents', [])
for item in contents:
key = item['Key']
if not extension or key.endswith(extension):
files.append(key.split('/')[-1])
except Exception as e:
print(f"Error listing files in {bucket_name}/{folder}: {e}")
return files
def move_files_multi_thread(bucket_name, src_folder, dest_folder, num_threads=10):
files = list_files(bucket_name, src_folder)
if not files:
print(f"No {extension} files found in the source folder.")
return
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [
executor.submit(move_file, bucket_name, src_folder, dest_folder, file_key)
for file_key in files
]
concurrent.futures.wait(futures)
print(f"All {extension} files have been moved.")
bucket_name = 'centene-national-contracting-files'
src_folder = 'batch6_16_file/redundant_files'
dest_folder = 'batch6_16_file/txt_files'
extension = '.txt'
move_files_multi_thread(bucket_name, src_folder, dest_folder, num_threads=40)
@@ -0,0 +1,54 @@
import csv
import boto3
from concurrent.futures import ThreadPoolExecutor
from botocore.exceptions import ClientError
s3 = boto3.Session(profile_name='temp_cred').client('s3')
def list_all_files(bucket_name):
all_keys = []
try:
paginator = s3.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=bucket_name):
if 'Contents' in page:
all_keys.extend([item['Key'] for item in page['Contents']])
except ClientError as e:
print(f"Error listing files in bucket {bucket_name}: {e}")
return all_keys
def check_file_in_bucket(filename, all_keys):
locations = [key for key in all_keys if (key.endswith(filename))]
if copy:
for l in locations:
s3.copy_object(
CopySource={'Bucket': bucket_name, 'Key': l},
Bucket=bucket_name,
Key=destination_prefix + "/" + filename
)
exists = bool(locations)
return filename, exists, locations
def process_csv(input_csv, output_csv, bucket_name):
with open(input_csv, mode='r', newline='', encoding='utf8') as infile, open(output_csv, mode='w', newline='', encoding='utf8') as outfile:
reader = csv.reader(infile)
writer = csv.writer(outfile)
writer.writerow(['File Name', 'exists', 'locations']) # Write header
with ThreadPoolExecutor() as executor:
futures = [executor.submit(check_file_in_bucket, row[0] + file_extension, all_keys) for row in reader]
for future in futures:
filename, exists, locations = future.result()
writer.writerow([filename, exists, locations])
input_csv = 'batch11/batch11_missing_files.csv'
output_csv = 'batch11/batch11_missing_files_movement_confirmation.csv'
bucket_name = 'centene-national-contracting-files'
destination_prefix = 'batch_11_priority_files/diff_files_v2'
copy = True
file_extension = '.txt'
all_keys = list_all_files(bucket_name)
process_csv(input_csv, output_csv, bucket_name)
print("CSV processing completed.")
@@ -0,0 +1,68 @@
import boto3
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
s3_client = boto3.Session(profile_name='temp_cred').client('s3')
def get_filenames_from_s3(bucket, prefix):
paginator = s3_client.get_paginator('list_objects_v2')
page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix)
filenames = set()
for page in page_iterator:
if 'Contents' in page:
for obj in page['Contents']:
filenames.add((obj['Key'].split('/')[-1])[:-4])
return filenames
def copy_file_if_exists(file_name, source_bucket, source_prefix, destination_bucket, destination_prefix, existing_files):
if file_name in existing_files:
source_key = f"{source_prefix}/{file_name}".strip('/') + ".pdf"
destination_key = f"{destination_prefix}/{file_name}".strip('/') + ".pdf"
# s3_client.copy_object(
# CopySource={'Bucket': source_bucket, 'Key': source_key},
# Bucket=destination_bucket,
# Key=destination_key
# )
response = s3_client.get_object(Bucket=source_bucket, Key=source_key)
file_content = response['Body'].read()
# print(f"Downloaded {source_key} from s3://{source_bucket}")
s3_client.put_object(Bucket=destination_bucket, Key=destination_key, Body=file_content)
print(f"Copied: {file_name} to {destination_prefix}")
else:
print(f"File not found: {file_name} in {source_prefix}")
rerun = pd.concat([rerun, df[df['File name Without Extension'] == file_name]])
def copy_files_multithreaded(file_list, source_bucket, source_prefix, destination_bucket, destination_prefix, existing_files, max_workers=20):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(copy_file_if_exists, file_name, source_bucket, source_prefix, destination_bucket, destination_prefix, existing_files)
for file_name in file_list
]
for future in as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Error copying file: {e}")
csv_path = 'new_duplicates_in_batch3.csv'
file_name_column = 'File Name Without Extension'
source_bucket = 'centene-national-contracting-files'
search_prefix = 'batch_3_priority_files/txt_files'
source_prefix = 'batch_3_priority_files/pdf_files'
destination_bucket = 'centene-national-contracting-files'
destination_prefix = 'batch_3_priority_files/re_run_files'
df = pd.read_csv(csv_path)
rerun = pd.DataFrame(columns=df.columns)
file_names = df[file_name_column].dropna().tolist()
print(f"Loaded {len(file_names)} file names from CSV.")
existing_files = get_filenames_from_s3(source_bucket, search_prefix)
print(f"Found {len(existing_files)} files in S3 source folder '{source_prefix}'.")
copy_files_multithreaded(file_names, source_bucket, source_prefix, destination_bucket, destination_prefix, existing_files)
rerun.reset_index(drop=True, inplace=True)
rerun.to_csv('batch3_rerun.csv')
+49
View File
@@ -0,0 +1,49 @@
import boto3
import re
import csv
from concurrent.futures import ThreadPoolExecutor
BUCKET_NAME = "centene-national-contracting-files"
PREFIX = "batch_1_priority_files/text_files/"
OUTPUT_CSV = "batch1/batch1_s3_files_page_counts.csv"
s3_client = boto3.Session(profile_name='temp_cred').client('s3')
def process_file(s3_key):
try:
response = s3_client.get_object(Bucket=BUCKET_NAME, Key=s3_key)
content = response['Body'].read().decode('utf-8')
matches = re.findall(r"Start of Page No\. = (\d+)", content)
last_page = matches[-1] if matches else None
return s3_key.split('/')[-1], last_page
except Exception as e:
print(f"Error processing {s3_key}: {e}")
return s3_key.split('/')[-1], None
def list_s3_objects(bucket, prefix):
paginator = s3_client.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get('Contents', []):
if obj['Key'].endswith('.txt'):
yield obj['Key']
try:
files = list(list_s3_objects(BUCKET_NAME, PREFIX))
except Exception as e:
print(f"Error listing objects in S3: {e}")
results = []
with ThreadPoolExecutor(max_workers= 50) as executor:
futures = [executor.submit(process_file, file) for file in files]
for future in futures:
results.append(future.result())
with open(OUTPUT_CSV, mode='w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["Filename", "Page Count"])
writer.writerows(results)
print(f"Output written to {OUTPUT_CSV}")
@@ -0,0 +1,44 @@
import boto3
import csv
"""
This is a basic script to list all files in a given S3 bucket and prefix.
There is need for listing out some file names in a given S3 bucket and prefix as client needs to reconcile the files.
There might not be any need for this script. But useful for future reference.
"""
def list_files_in_s3(bucket_name, s3_prefix, profile_name, output_csv):
# Initialize a session using a custom profile
session = boto3.Session(profile_name=profile_name)
s3_client = session.client('s3')
# List all files in the given S3 bucket and prefix
paginator = s3_client.get_paginator('list_objects_v2')
operation_parameters = {
'Bucket': bucket_name,
'Prefix': s3_prefix
}
# Open the CSV file for writing
with open(output_csv, mode='w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['file_name', 'sub_prefix'])
# Paginate through all files
for page in paginator.paginate(**operation_parameters):
if 'Contents' in page:
for obj in page['Contents']:
key = obj['Key']
# Extract the sub-prefix by removing the base prefix from the key
sub_prefix = key[len(s3_prefix):].split('/')[1] if len(key[len(s3_prefix):].split('/')) > 1 else ''
writer.writerow([key, sub_prefix])
print(f"Found: {key}")
if __name__ == "__main__":
# Specify the S3 bucket, prefix, profile name, and output CSV file
bucket_name = "centene-texas-files" # Update your S3 bucket name
s3_prefix = "bedrock_rerun_text_files/not_executed/" # Update the S3 prefix (folder path in S3)
profile_name = "" # Update the AWS CLI profile name
output_csv = "not_executed_tx_rerun_list.csv" # Update the output CSV file name
list_files_in_s3(bucket_name, s3_prefix, profile_name, output_csv)
@@ -0,0 +1,182 @@
import pandas as pd
import os
import boto3
"""
This script is used to match file names between a local CSV and an Excel sheet, and copy the matched files to a specified prefix in an S3 bucket using boto3. The script performs the following main steps:
1. **Loading Files**: Load all files from the CSV and subset files from the Excel sheet.
2. **Normalizing File Names**: Normalize file names for proper matching by standardizing formats.
3. **Matching Files**: Identify matching and missing files between the two data sources.
4. **Copying Matched Files**: Copy matched files to a specific prefix in an S3 bucket.
The script uses multiple sheets in an Excel file, processes each one, and handles S3 operations for moving matched files.
"""
# -------------------------------
# Configuration Variables
# -------------------------------
# File paths
# s3://centene-texas-files/missc-tx-files/all_files.csv
all_files_path = 'C:\\Doczy\\Centene Texas\\all_files.csv' # Path to the CSV containing all file names
excel_path = 'additional-9k.xlsx' # Path to the Excel file containing subset file names from client
sheet_names = ['Sheet1'] # List of sheet names to be processed. This depends on what the client team shares
# AWS Configuration
profile_name = '' # AWS CLI profile name for authentication
bucket_name = 'centene-texas-files' # S3 bucket name. This script is mainly used only with Texas files
# Destination prefixes in S3
new_prefix_map = {
'Sheet1': 'bedrock_rerun_text_files/not_executed/rerun_20241125/'
# WE CAN USE THIS MAP IF THERE ARE MULTIPLE SHEETS TO BE PROCESSED
# 'Missing Row': 'bedrock_rerun_text_files/not_executed/b_missing_row_output/',
# 'IRS #': 'bedrock_rerun_text_files/not_executed/a_c_blank_irs/'
}
# -------------------------------
# Functions
# -------------------------------
def load_files(all_files_path, excel_path, sheet_name):
"""
Loads all files from CSV and subset files from Excel sheet.
:param all_files_path: Path to the CSV file containing all file names
:param excel_path: Path to the Excel file containing subset file names
:param sheet_name: Sheet name in the Excel file to load
:return: DataFrames for all files and subset files
"""
all_files_df = pd.read_csv(all_files_path) # Load all files from CSV
subset_files_df = pd.read_excel(excel_path, sheet_name=sheet_name, header=1) # Load subset files from specified Excel sheet
return all_files_df, subset_files_df
def normalize_file_names(all_files_df, subset_files_df):
"""
Normalizes file names in both DataFrames for matching.
:param all_files_df: DataFrame containing all file names
:param subset_files_df: DataFrame containing subset file names
:return: Normalized DataFrames for all files and subset files
"""
# Append '.txt' to CONTRACT NAME in the subset files DataFrame to create a consistent file name format
subset_files_df['File Name '] = subset_files_df['CONTRACT NAME'].apply(lambda x: x + '.txt')
# Remove leading hyphen from base_name if present and convert to lowercase for normalization
all_files_df['base_name'] = all_files_df['base_name'].fillna('').apply(lambda x: x[1:] if isinstance(x, str) and x.startswith('-') else x)
all_files_df['base_name'] = all_files_df['base_name'].str.lower()
# Convert file names in subset DataFrame to lowercase for normalization
subset_files_df['File Name '] = subset_files_df['File Name '].str.lower()
return all_files_df, subset_files_df
def match_files(all_files_df, subset_files_df):
"""
Matches files between the all files DataFrame and the subset files DataFrame.
:param all_files_df: DataFrame containing all file names
:param subset_files_df: DataFrame containing subset file names
:return: DataFrames for matched files and missing files
"""
# Find all files in all_files_df that have a base name matching any file name in subset_files_df
matched_files_df = all_files_df[all_files_df['base_name'].isin(subset_files_df['File Name '])]
# Find all files in subset_files_df that do not have a matching base name in all_files_df
missing_files_df = subset_files_df[~subset_files_df['File Name '].isin(all_files_df['base_name'])]
return matched_files_df, missing_files_df
def process_multiple_sheets(all_files_path, excel_path, sheet_names):
"""
Processes multiple sheets from the Excel file and matches files with the CSV.
:param all_files_path: Path to the CSV file containing all file names
:param excel_path: Path to the Excel file containing subset file names
:param sheet_names: List of sheet names to be processed
:return: Dictionary containing matched and missing files DataFrames for each sheet
"""
results = {}
for sheet in sheet_names:
print(f"Processing Sheet: {sheet}")
# Load all files and subset files for the current sheet
all_files_df, subset_files_df = load_files(all_files_path, excel_path, sheet)
# Normalize file names for both DataFrames
all_files_df, subset_files_df = normalize_file_names(all_files_df, subset_files_df)
# Match files between the two DataFrames
matched_files_df, missing_files_df = match_files(all_files_df, subset_files_df)
# Store the results for each sheet in the dictionary
results[sheet] = {
'matched_files_df': matched_files_df,
'missing_files_df': missing_files_df
}
return results
def copy_files_to_new_prefix(matched_files_df, bucket_name, destination_prefix):
"""
Copies matched files to a new prefix in the specified S3 bucket.
:param matched_files_df: DataFrame containing matched file information
:param bucket_name: S3 bucket name
:param destination_prefix: Prefix in the S3 bucket to copy files to
"""
# Create a boto3 session and S3 client
session = boto3.Session(profile_name=profile_name)
s3 = session.client('s3')
# Iterate over each matched file and copy it to the new destination prefix
for _, row in matched_files_df.iterrows():
source_key = row['file_name'] # Source key in S3 bucket
destination_key = destination_prefix + os.path.basename(source_key) # Destination key with new prefix
print(f"Copying {source_key} to {destination_key}")
try:
# Define the copy source and perform the copy operation
copy_source = {'Bucket': bucket_name, 'Key': source_key}
s3.copy(copy_source, bucket_name, destination_key)
print(f"Copied {source_key} to {destination_key}")
except Exception as e:
# Handle any exceptions that occur during the copy process
print(f"FILE NOT FOUND: {e}")
# -------------------------------
# Main Script
# -------------------------------
# Process multiple sheets and get results
results = process_multiple_sheets(all_files_path, excel_path, sheet_names)
# Combine matched and missing files from all sheets
combined_df = pd.DataFrame() # DataFrame to store all matched files
missing_files_df = pd.DataFrame() # DataFrame to store all missing files
for sheet, data in results.items():
print(f'{sheet}:')
print(f'Matched files: {data["matched_files_df"].shape[0]}') # Print the number of matched files for the sheet
print(f'Missing files: {data["missing_files_df"].shape[0]}') # Print the number of missing files for the sheet
print(data['missing_files_df']['CONTRACT NAME'].values) # Print the names of the missing files
# Append matched and missing files to the combined DataFrames
combined_df = pd.concat([combined_df, data['matched_files_df']], ignore_index=True)
missing_files_df = pd.concat([missing_files_df, data['missing_files_df']], ignore_index=True)
# Copy matched files to the appropriate prefix in S3
for sheet, data in results.items():
matched_files_df = data['matched_files_df'] # DataFrame containing matched files for the sheet
destination_prefix = new_prefix_map.get(sheet) # Get the destination prefix for the sheet
if destination_prefix:
# Copy matched files to the new prefix in S3
copy_files_to_new_prefix(matched_files_df, bucket_name, destination_prefix)
else:
print(f"No destination prefix found for sheet: {sheet}")
print("File copying complete.")
@@ -0,0 +1,173 @@
import boto3
import pandas as pd
from datetime import datetime, timedelta
"""
This script is a work in progress.
TODO:
1. Read the DS batch tracker and get the total document counts per client and hence calculate the total bedrock cost per client.
This script is used to automate the process of fetching AWS costs for specific services and additional charges, as well as counting the number of records in each client's S3 bucket.
The script performs the following main steps:
1. **Get Service Costs**: Fetch costs for specified AWS services using the AWS Cost Explorer API.
2. **Get Costs by Record Type**: Fetch additional charges (e.g., Tax, Support) using the AWS Cost Explorer API.
3. **Process Bucket**: Count the number of records in each client's S3 bucket for a specified date range.
4. **Export Data**: Export the cost data and bucket record counts to CSV files.
"""
def get_service_costs(start_date, end_date, services, profile_name):
client = boto3.Session(profile_name=profile_name).client('ce') # Cost Explorer client
response = client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date # AWS Cost Explorer 'End' date is exclusive
},
Granularity='DAILY',
Metrics=['UnblendedCost'],
GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}],
Filter={
'Dimensions': {
'Key': 'SERVICE',
'Values': services
}
}
)
costs = {}
for result in response['ResultsByTime']:
for group in result.get('Groups', []):
service = group['Keys'][0]
amount = float(group['Metrics']['UnblendedCost']['Amount'])
costs[service] = costs.get(service, 0) + amount
return costs
def get_costs_by_record_type(start_date, end_date, record_types, profile_name):
client = boto3.Session(profile_name=profile_name).client('ce')
response = client.get_cost_and_usage(
TimePeriod={'Start': start_date, 'End': end_date},
Granularity='DAILY',
Metrics=['UnblendedCost'],
GroupBy=[{'Type': 'DIMENSION', 'Key': 'RECORD_TYPE'}],
Filter={
'Dimensions': {
'Key': 'RECORD_TYPE',
'Values': record_types
}
}
)
costs = {}
for result in response['ResultsByTime']:
for group in result.get('Groups', []):
record_type = group['Keys'][0]
amount = float(group['Metrics']['UnblendedCost']['Amount'])
costs[record_type] = costs.get(record_type, 0) + amount
return costs
def process_bucket(bucket_name, start_date_obj, end_date_obj, profile_name):
s3 = boto3.Session(profile_name=profile_name).client('s3')
prefix = 'processed_batch_ids/'
paginator = s3.get_paginator('list_objects_v2')
page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix)
total_records = 0
for page in page_iterator:
for obj in page.get('Contents', []):
key = obj['Key']
last_modified = obj['LastModified']
if key.endswith('.csv') and start_date_obj <= last_modified.date() <= end_date_obj:
# Read the CSV file into a dataframe
csv_obj = s3.get_object(Bucket=bucket_name, Key=key)
df = pd.read_csv(csv_obj['Body'])
total_records += len(df)
# For drilling down
# print(f"Number of records in {key}: {len(df)}")
return total_records
def main():
# Input parameters
start_date = '2024-11-01' # Replace with your start date
end_date = '2024-11-25' # Replace with your end date
client_buckets = ['highmark-files', 'la-care-files',
'centene-fidelis-files',
'centene-healthnet-files',
'centene-national-contracting-files',
'centene-texas-files',
'bcbs-files',
'community-health-choice-files',
'humana-files',
'scan-health-files',
'texas-children-files',
'village-care']
profile_name = 'doczy_uat'
# List of services to fetch costs for
services = [
'Amazon Textract',
'Amazon Bedrock',
'Claude Instant (Amazon Bedrock Edition)',
'Claude 3 Haiku (Amazon Bedrock Edition)',
'Claude 3.5 Sonnet (Amazon Bedrock Edition)'
]
# Convert date strings to datetime objects
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
# Adjust end_date for AWS Cost Explorer API (end date is exclusive)
end_date_ce = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')
# Get costs for specified services
service_costs = get_service_costs(start_date, end_date_ce, services, profile_name)
# Get charges for Tax and Support
tax_costs = get_costs_by_record_type(start_date, end_date_ce, ['Tax'], profile_name)
support_costs = get_costs_by_record_type(start_date, end_date_ce, ['Support'], profile_name)
# Prepare data for CSV export
cost_data = []
# Output costs
print("AWS Service Costs:")
for service in services:
cost = service_costs.get(service, 0.0)
print(f" {service}: ${cost:.2f}")
cost_data.append({'Category': 'Service', 'Item': service, 'Cost': cost})
print("\nAdditional Charges:")
tax_cost = tax_costs.get('Tax', 0.0)
support_cost = support_costs.get('Support', 0.0)
print(f" Tax: ${tax_cost:.2f}")
print(f" Support: ${support_cost:.2f}")
cost_data.append({'Category': 'Additional Charges', 'Item': 'Tax', 'Cost': tax_cost})
cost_data.append({'Category': 'Additional Charges', 'Item': 'Support', 'Cost': support_cost})
total_costs = sum([cost['Cost'] for cost in cost_data])
cost_data.append({'Category': 'Total', 'Item': 'Total', 'Cost': total_costs})
# Process each bucket and count records
print("\nBucket-wise Record Counts:")
bucket_data = []
for bucket in client_buckets:
total_records = process_bucket(bucket, start_date_obj, end_date_obj, profile_name)
print(f" {bucket}: {total_records} records")
bucket_data.append({'Bucket': bucket, 'Records': total_records})
# Option to export data as CSV
export_csv = True # Set to True to export CSV
if export_csv:
# Export cost data
cost_df = pd.DataFrame(cost_data)
cost_df.to_csv('aws_costs.csv', index=False)
print("\nService costs exported to 'aws_costs.csv'.")
# Export bucket data
bucket_df = pd.DataFrame(bucket_data)
bucket_df.to_csv('bucket_records.csv', index=False)
print("Bucket record counts exported to 'bucket_records.csv'.")
if __name__ == '__main__':
main()
@@ -0,0 +1,64 @@
import boto3
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
"""
This script is used to copy complex contract text files to a different folder in the same S3 bucket.
The script reads a CSV file that contains the filenames and their table counts. It filters the rows where the table count is greater than or equal to 1.
This script is NOT CLIENT SPECIFIC. It is a generic script that can be used for any client.
"""
# Function to copy a single file
def copy_file(bucket_name, source_prefix, dest_prefix, file_name, profile_name):
try:
# Initialize S3 client
session = boto3.Session(profile_name=profile_name)
s3 = session.client('s3')
copy_source = {'Bucket': bucket_name, 'Key': f'{source_prefix}/{file_name}'}
dest_key = f'{dest_prefix}/{file_name}'
s3.copy_object(CopySource=copy_source, Bucket=bucket_name, Key=dest_key)
print(f'Successfully copied from {copy_source} to {dest_key}')
except Exception as e:
# print(f'Failed to copy {file_name}: {e}')
print('File not in bucket. Skipping')
pass
# Function to process copying in parallel
def copy_files_in_parallel(bucket_name, source_prefix, dest_prefix, file_list,profile_name ,max_workers=10):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(copy_file, bucket_name, source_prefix, dest_prefix, file_name, profile_name=profile_name)
for file_name in file_list
]
for future in as_completed(futures):
future.result() # This will raise any exceptions that occurred during execution
# Main function
def main():
# Client bucket where the text files are stored
bucket_name = 'centene-national-contracting-files'
# This is used to copy complex contract text files to a different folder
source_prefix = 'batch6_16_file/txt_files'
dest_prefix_3a = 'batch6_16_file/complex_contract_files/txt_files'
max_workers = 50
profile_name = 'default' # Set this as per your AWS profile
# Read the table analysis file (from DS code) and read the filenames and their table counts
csv_file = 'C:\\Doczy\\National contracting\\Somefolder\\CNC-6to16-Table-Analysis.csv' # Replace with your CSV file path
file_df = pd.read_csv(csv_file)
# Filter rows where 'tables' value is greater than or equal to 1
complex_table_files = file_df[file_df['Table Count'] >= 1]
# Assuming the CSV has a column 'file_name' with the list of file names
file_list = complex_table_files['Filename_pdf'].tolist()
copy_files_in_parallel(bucket_name, source_prefix, dest_prefix_3a, file_list, profile_name = profile_name, max_workers=max_workers)
if __name__ == '__main__':
main()
@@ -0,0 +1,308 @@
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()
+197
View File
@@ -0,0 +1,197 @@
import boto3
import pandas as pd
from io import StringIO
import os
import json
import requests
"""
This script is used to process files from a source S3 bucket and trigger the pipeline for processing.
The script does the following:
1. Load files from the source S3 bucket based on the directory prefix
2. Create a batch in the client bucket
3. Copy files to the client bucket and add tags
4. Move the original files to a processed folder in the source bucket
5. Create a contract list and send it to the pipeline API
6. Update the CSV with the processed batch details
7. Trigger the pipeline API
The script uses the following variables:
- source_bucket: The source S3 bucket name
(Client bucket where all source files are located.
NOTE: This function will move the files to processed folder in the source bucket, so the source prefix will be empty after the execution)
- api_trigger_url: The URL of the pipeline trigger API ( This explicit url can be reaplced by using AWS SDK to get the API URL)
- create_batch_url: The URL of the create batch API
- csv_file: The name of the CSV file to store processed batch details
(Usually we add relevant bucket of files to track this, e.g. Facility_files.csv )
- processed_csv_s3_key: The S3 key to store the processed CSV file
- client_bucket: The client S3 bucket name
- directory_prefix_list: A list of directory prefixes to search for files in the source bucket
- client_name: The name of the client (for logs)
- username: The username of the user triggering the pipeline (for logs)
The script can be scheduled to run periodically to process files from the source bucket.
Add this code to a lambda function, ensure there is a lambda layer for pandas
AWS provides a layer for pandas, you can use that or create your own layer (AWSSDKPandas-Python310)
Once the function is created, we can create an event trigger for the lambda function to run at a specific time or a a specific interval.
Note: The script assumes that the pipeline trigger API and create batch API are already set up and working correctly.
"""
source_bucket = 'centene-texas-files'
api_trigger_url = 'https://<APISTAGE>.execute-api.us-east-2.amazonaws.com/dev/trigger-pipeline'
create_batch_url = "https://<APISTAGE>.execute-api.us-east-2.amazonaws.com/dev/create-batch"
csv_file = 'ancillary_files.csv' #file count need to as prefix
processed_csv_s3_key = f'processed_batch_ids/{csv_file}'
client_bucket = 'doczyai-use2-u-cn1-s3-textract-processing-001'
client_name = 'Centene-texas'
username = '<Username>'
directory_prefix_list = [
'for-textract/'
# Can add more prefixes here if needed
# 'TX-Legacy-agreements_2/batch2/for-processing/',
# 'TX-Legacy-agreements_2/batch2/for-processing/',
# 'TX-Legacy-agreements_3/batch3/for-processing/',
# 'TX-Legacy-agreements_4/batch4/for-processing/',
# 'TX-Legacy-Amendments/TX-Legacy-Amendments/for-processing/'
]
def lambda_handler(event, context):
s3_resource = boto3.resource('s3')
for directory_prefix in directory_prefix_list:
bucket_obj = s3_resource.Bucket(source_bucket)
files = [obj.key for obj in bucket_obj.objects.filter(Prefix=directory_prefix) if obj.key.endswith('.Pdf') or obj.key.endswith('.pdf')]
if files:
print(f"Processing files for directory prefix: {directory_prefix}")
process_files(source_bucket, directory_prefix, files[:60], create_batch_url, api_trigger_url)
break
else:
print(f"No files found for directory prefix: {directory_prefix}. Moving to the next prefix...")
def process_files(source_bucket, directory_prefix, files, create_batch_url, api_trigger_url):
s3_client = boto3.client('s3')
# Load existing CSV from S3 if it exists
existing_df = None
try:
csv_obj = s3_client.get_object(Bucket=source_bucket, Key=processed_csv_s3_key)
existing_df = pd.read_csv(csv_obj['Body'])
print(f"Loaded existing CSV with {len(existing_df)} records")
except s3_client.exceptions.NoSuchKey:
print("No existing CSV found. A new one will be created.")
batch_id, landing_zone = create_batch(client_bucket, create_batch_url)
if batch_id == 'failed_cases':
print("Batch creation failed. Skipping this batch...")
return
contract_list = []
for s3_key in files:
filename = os.path.basename(s3_key).replace('.Pdf', '.pdf')
new_s3_key = f'contracts-landing-zone/{batch_id}/{filename}'
# Copy the file to the new key with .pdf extension
copy_source = {'Bucket': source_bucket, 'Key': s3_key}
s3_client.copy_object(CopySource=copy_source, Bucket=client_bucket, Key=new_s3_key)
# Add tags to the copied file
s3_client.put_object_tagging(
Bucket=client_bucket,
Key=new_s3_key,
Tagging={
'TagSet': [
{
'Key': 'BatchId',
'Value': batch_id
}
]
}
)
# Move the original file to the processed subfolder
processed_s3_key = f'processed/{os.path.basename(s3_key)}'
s3_client.copy_object(CopySource=copy_source, Bucket=source_bucket, Key=processed_s3_key)
s3_client.delete_object(Bucket=source_bucket, Key=s3_key)
# Add file details to contract list
contract_list.append({
"contract_name": filename,
"groups": ["A", "C"], # This should be dynamic based on your requirements
"contract_source_path": new_s3_key
})
print(f"Uploaded {len(files)} files to S3 bucket for batch id {batch_id}")
data = {
"s3_bucket": client_bucket,
"batch_id": batch_id,
"client_name": client_name,
"username": username,
"contract_list": contract_list
}
# Convert contract list to DataFrame and append to existing DataFrame
new_df = pd.DataFrame(contract_list)
new_df['batch_id'] = batch_id
if existing_df is not None:
existing_df = pd.concat([existing_df, new_df])
else:
existing_df = new_df
# Upload updated CSV back to S3
csv_buf = StringIO()
existing_df.to_csv(csv_buf, header=True, index=False)
csv_buf.seek(0)
s3_client.put_object(Bucket=source_bucket, Body=csv_buf.getvalue(), Key=processed_csv_s3_key)
print(f"Saved updated batch details to {processed_csv_s3_key} and sending to API...")
response = requests.post(api_trigger_url, json=data)
print(response.text)
print(response.status_code)
return {
"statusCode": 200,
"body": json.dumps("Processed files successfully")
}
def create_batch(client_bucket, create_batch_url):
myobj = { "client-bucket-name": client_bucket }
response = requests.post(create_batch_url, json=myobj)
if response.status_code >= 200 and response.status_code < 300:
try:
response_body = json.loads(json.loads(response.text)['body'])
new_batch_id = response_body['batch_id']
landing_zone = response_body['landing_zone']
except:
print(myobj)
print(response.text)
new_batch_id = 'failed_cases'
landing_zone = 'contracts_landing_zone'
else:
print(response.text)
new_batch_id = 'failed_cases'
landing_zone = 'contracts_landing_zone'
return new_batch_id, landing_zone
@@ -0,0 +1,181 @@
import boto3
import os
import pandas as pd
from datetime import datetime
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
"""
This script is used to move files from one S3 bucket to another S3 bucket based on the batch_ids provided in the CSV file.
The script will move the text files, stuck files, PDF files and invalid files to the destination bucket.
The copy operation is done using multithreading to speed up the process. Use the max_workers parameter to adjust the number of threads.
This script is NOT CLIENT SPECIFIC. It is a generic script that can be used for any client.
Usage:
1. Update the configuration section with the required parameters.
2. Run the script.
Note:
- Ensure you have the necessary permissions to read from the source bucket and write to the destination bucket (Admin role is preferred).
"""
def log_error_to_file(key, error_message, error_log_file):
# Log error to file
# Create new file if it doesnt exist
if not os.path.exists(error_log_file):
with open(error_log_file, 'w') as f:
f.write("Error Log\n")
f.write("---------\n")
with open(error_log_file, 'a') as f:
f.write(f"Error with file: {key} - {error_message}\n")
def list_filtered_files(bucket_name, prefix, start_date, end_date, profile_name):
start_date = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
end_date = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
session = boto3.Session(profile_name=profile_name)
s3_client = session.client('s3')
paginator = s3_client.get_paginator('list_objects_v2')
page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix)
filtered_files = []
for page in page_iterator:
if 'Contents' in page:
for obj in page['Contents']:
last_modified = obj['LastModified']
if start_date <= last_modified <= end_date:
filtered_files.append(obj['Key'])
return filtered_files
def copy_single_file(s3_client, source_bucket, destination_bucket, key, destination_prefix, error_log_file):
try:
copy_source = {'Bucket': source_bucket, 'Key': key}
destination_key = os.path.join(destination_prefix, os.path.basename(key))
print(f"Copying {key} to {destination_key}")
s3_client.copy(copy_source, destination_bucket, destination_key)
except Exception as e:
error_message = str(e)
print(f"Error copying file {key}: {error_message}")
# Log error to file
log_error_to_file(key, error_message, error_log_file)
def copy_files_to_destination(source_bucket, destination_bucket, file_keys, profile_name, destination_prefix, max_workers, error_log_file):
session = boto3.Session(profile_name=profile_name)
s3_client = session.client('s3')
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(copy_single_file, s3_client, source_bucket, destination_bucket, key, destination_prefix, error_log_file) for key in file_keys]
for future in as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Error in thread: {e}")
print("Copy completed.")
def main():
# ------------------------------ Configuration ------------------------------
# Error log file path
# There is usually no errors in this script, but in case of any errors, they will be logged to this file
error_log_file = "error_log_batch5.txt"
# Main script
# Fetch the batch_ids csv file from the client bucket ('centene-national-contracting-files')/processed_batch_ids/ folder
csv_file_path = "batch5_diff_files.csv"
# Source and destination bucket names. Where source bucket is the textract pipeline where all files reside in their batch folders
source_bucket = 'doczyai-use2-u-cn1-s3-textract-processing-001'
# Destination bucket is the client bucket where we move the files to satge it for DS team
destination_bucket = 'centene-national-contracting-files'
# Depending on what batch files we want to move, we can change the destination prefix
destination_prefix = 'batch_5_priority_files/txt_files/'
# If the batch size in scheduler is large, there is a possibility of throttling and files getting stuck in the textract pipeline
# We move these stuck files to a separate folder so that it can be sent for reprocessing
stuck_destination_prefix = 'batch_5_priority_files/throttled_files_final/'
# PDF files are required for the MCS team to review the contracts
pdf_files_destination = f'batch_5_priority_files/pdf_files/'
# Less than 1% of the files are invalid files. These files are moved to a separate folder
invalid_files_destination = f'batch_5_priority_files/invalid_files/'
# Date range for files to be moved so that we can limit the list files in bucket operation
start_date = '2024-07-07T00:00:00Z' # ISO 8601 format
end_date = '2024-12-31T23:59:59Z' # ISO 8601 format
# If you are using AWS CLI profiles, provide the profile name here to authenticate
profile_name = 'doczy_uat'
max_workers = 50 # Adjust the number of threads as needed
# ------------------------------ Script ------------------------------
batch_ids_df = pd.read_csv(csv_file_path)
batch_ids = batch_ids_df['batch_id'].unique()
print(f"Total number of batches: {len(batch_ids)}")
print(f"Batch IDs: {batch_ids}")
text_count = 0
stuck_count = 0
pdf_count = 0
invalid_count = 0
# Bulk copy files from S3 for all batch_ids
for batch_id in batch_ids:
prefix = f'contract-text-file/{batch_id}/'
stuck_files_path = f'textract-sender-staging-pdfs/{batch_id}/'
pdf_files_path = f'textract-receiver-processed-pdfs/{batch_id}'
invalid_files_path = f'invalid-pdfs/{batch_id}'
# List filtered text files
filtered_files = list_filtered_files(source_bucket, prefix, start_date, end_date, profile_name)
print(f"Batch_id: {batch_id} Filtered files: {len(filtered_files)}")
text_count += len(filtered_files)
# Throttled files list
stuck_files = list_filtered_files(source_bucket, stuck_files_path, start_date, end_date, profile_name)
print(f"Batch_id: {batch_id} Stuck files: {len(stuck_files)}")
stuck_count += len(stuck_files)
# PDF file list
pdf_files = list_filtered_files(source_bucket, pdf_files_path, start_date, end_date, profile_name)
print(f"Batch_id: {batch_id} PDF files: {len(pdf_files)}")
pdf_count += len(pdf_files)
# Invalid files list
invalid_files = list_filtered_files(source_bucket, invalid_files_path, start_date, end_date, profile_name)
print(f"Batch_id: {batch_id} Invalid files: {len(invalid_files)}")
invalid_count += len(invalid_files)
# Copy files to destination using multithreading
copy_files_to_destination(source_bucket, destination_bucket, filtered_files, profile_name, destination_prefix, max_workers, error_log_file)
copy_files_to_destination(source_bucket, destination_bucket, stuck_files, profile_name, stuck_destination_prefix, max_workers, error_log_file)
copy_files_to_destination(source_bucket, destination_bucket, pdf_files, profile_name, pdf_files_destination, max_workers, error_log_file)
copy_files_to_destination(source_bucket, destination_bucket, invalid_files, profile_name, invalid_files_destination, max_workers, error_log_file)
print(f"Total text files copied: {text_count}")
print(f"Total stuck files copied: {stuck_count}")
print(f"Total pdf files copied: {pdf_count}")
print(f"Total invalid files copied: {invalid_count}")
if __name__ == '__main__':
main()