Merged in feature/ops_scripts (pull request #333)

Feature/ops scripts

* Added comments to Aryan's script and added some more scripts

* Search and Copy Script uploaded as a Python Notebook - with comments and markdown

* Merged main into feature/ops_scripts

* Merged main into feature/ops_scripts


Approved-by: Michael McGuinness
Approved-by: Chris Stobie
This commit is contained in:
Umang Shailesh Mistry
2024-12-17 11:18:41 +00:00
committed by Michael McGuinness
parent 91d3680ee4
commit c210052952
13 changed files with 882 additions and 8 deletions
+12
View File
@@ -3,6 +3,18 @@ import csv
import os
import pandas as pd
"""
This script is used for CNC diff analysis. It compares two folders (source1 and source2) and writes the comparison results to a CSV file.
source1_type and source2_type should be one of the following:
- local: for local directories
- s3: for S3 folders
- csv: for CSV files
S3 credentials should be stored in the AWS credentials file under the profile name 'temp_cred' or can be changed in the aws credentials set.
"""
def list_s3_files(bucket_name, folder):
s3 = boto3.Session(profile_name='temp_cred').client('s3')
paginator = s3.get_paginator('list_objects_v2')
+14
View File
@@ -4,6 +4,20 @@ import glob
from PyPDF2 import PdfReader
from concurrent.futures import ThreadPoolExecutor, as_completed
"""
This script will collect the following information for all PDF files in a directory and its subdirectories:
- File Name
- File Path
- Page Count
The information will be saved in a CSV file.
NOTE: This script is required mainly in the pre-doczy analysis. There might not be need for this since CNC project is already processed through the textract pipeline.
"""
def get_pdf_info(pdf_path):
try:
with open(pdf_path, 'rb') as file:
@@ -4,6 +4,17 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from collections import defaultdict
import csv
"""
This script searches for files in a given directory based on a list of filenames in a CSV file.
The CSV file should have a column named 'filename' containing the filenames to search for.
This is maily used to find files that are not present in the s3 or cannot be located.
In that case, we use the list of missing files (in the CSV) to search for them in the T drive.
This script may not be needed as all CNC batches have been staged for execution.
"""
def build_file_cache(base_directory):
file_cache = defaultdict(list)
+8
View File
@@ -1,5 +1,13 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is an adhoc python notebook that mainly does some dups analysis, file movements and debugging. \n",
"This is not something that is usually repeated and is pushed only for reference. "
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -1,6 +1,19 @@
import boto3
import concurrent.futures
"""
This script moves files with a specific extension from one folder to another in an S3 bucket.
It uses multiple threads to move files in parallel.
The script uses a temporary AWS profile called 'temp_cred' to authenticate with AWS.
The profile should be configured in the ~/.aws/credentials file.
Mainly used to copy and delete (move) files in the same bucket. Typically used to move redundant dups back to the batch TXT folder.
Can be used to copy .txt / .pdf files from one folder to another in the same bucket.
"""
s3 = boto3.Session(profile_name='temp_cred').client('s3')
def move_file(bucket_name, src_folder, dest_folder, file_key):
@@ -0,0 +1,191 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Setting up imports that we will need, particularly - csv, boto3, ThreadPoolExecutor and pandas"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {},
"outputs": [],
"source": [
"import csv\n",
"import boto3\n",
"from concurrent.futures import ThreadPoolExecutor\n",
"from botocore.exceptions import ClientError\n",
"import pandas as pd\n",
"\n",
"s3 = boto3.Session(profile_name='temp_cred').client('s3')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This function use pagination to iterate through all objects in the specified s3 bucket and builds a file cache with all s3 keys. This is meant to be run only-once and is expected to take 8-10 mins to complete"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [],
"source": [
"def list_all_files(bucket_name):\n",
" all_keys = []\n",
" try:\n",
" paginator = s3.get_paginator('list_objects_v2')\n",
" for page in paginator.paginate(Bucket=bucket_name):\n",
" if 'Contents' in page:\n",
" all_keys.extend([item['Key'] for item in page['Contents']])\n",
" except ClientError as e:\n",
" print(f\"Error listing files in bucket {bucket_name}: {e}\")\n",
" return all_keys\n",
"\n",
"bucket_name = 'centene-national-contracting-files'\n",
"all_keys = list_all_files(bucket_name)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Searching v1 - using complete matching of the filenames in the s3 bucket. If ```copy``` is set to ```True```, then the files found will also be copied to the specified prefix."
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CSV processing completed.\n"
]
}
],
"source": [
"# Complete matching\n",
"def check_file_in_bucket(filename, all_keys):\n",
" locations = [key for key in all_keys if (key.lower().endswith(filename.lower()))]\n",
" if copy:\n",
" for l in locations:\n",
" s3.copy_object(\n",
" CopySource={'Bucket': bucket_name, 'Key': l},\n",
" Bucket=bucket_name,\n",
" Key=destination_prefix + \"/\" + filename\n",
" )\n",
" exists = bool(locations)\n",
" return filename, exists, locations\n",
"\n",
"def process_csv(input_csv, output_csv):\n",
" \n",
" with open(input_csv, mode='r', newline='', encoding='utf8') as infile, open(output_csv, mode='w', newline='', encoding='utf8') as outfile:\n",
" reader = csv.reader(infile)\n",
" writer = csv.writer(outfile)\n",
" writer.writerow(['File Name', 'exists', 'locations']) # Write header\n",
"\n",
" with ThreadPoolExecutor(max_workers = 50) as executor:\n",
" futures = [executor.submit(check_file_in_bucket, row[0][:-4] + file_extension, all_keys) for row in reader]\n",
" \n",
" for future in futures:\n",
" filename, exists, locations = future.result()\n",
" writer.writerow([filename, exists, locations])\n",
"\n",
"# Input csv with all file names listed without extension in the first column\n",
"input_csv = '../CNC/batch4/batch4_missing_files_2.csv'\n",
"# Path to output that will be generated with the following columns = File Name, exists, locations\n",
"output_csv = '../CNC/batch4/batch4_missing_files_2_paths_txt.csv'\n",
"# If copy = True, the files found will be copied over to this destination in s3 (User Keys needs copy permission)\n",
"destination_prefix = 'batch_4_priority_files/hotfix_diff_121124/TXT_FILES'\n",
"copy = True\n",
"# File Extension that will be added to the end of the file names - to look for\n",
"file_extension = '.txt'\n",
"\n",
"process_csv(input_csv, output_csv)\n",
"print(\"CSV processing completed.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Searching v2 - using partial matching of the filenames in the s3 bucket. In this specific case, we check the first 10 and last 10 characters of the filename (This logic can be changed in the 3rd line, as required). If ```copy``` is set to ```True```, then the files found will also be copied to the specified prefix."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Partial matching\n",
"def check_file_in_bucket(filename, all_keys):\n",
" locations = [key for key in all_keys if (filename[:10].lower() in key.lower() and key.lower().endswith(filename[-10:].lower()))]\n",
" if copy:\n",
" for l in locations:\n",
" s3.copy_object(\n",
" CopySource={'Bucket': bucket_name, 'Key': l},\n",
" Bucket=bucket_name,\n",
" Key=destination_prefix + \"/\" + filename\n",
" )\n",
" exists = bool(locations)\n",
" return filename, exists, locations\n",
"\n",
"def process_csv(input_csv, output_csv):\n",
" \n",
" with open(input_csv, mode='r', newline='', encoding='utf8') as infile, open(output_csv, mode='w', newline='', encoding='utf8') as outfile:\n",
" reader = csv.reader(infile)\n",
" writer = csv.writer(outfile)\n",
" writer.writerow(['File Name', 'exists', 'locations']) # Write header\n",
"\n",
" with ThreadPoolExecutor(max_workers = 50) as executor:\n",
" futures = [executor.submit(check_file_in_bucket, row[0][:-4] + file_extension, all_keys) for row in reader]\n",
" \n",
" for future in futures:\n",
" filename, exists, locations = future.result()\n",
" writer.writerow([filename, exists, locations])\n",
"\n",
"# Input csv with all file names listed without extension in the first column\n",
"input_csv = '../CNC/batch4/batch4_missing_files_2.csv'\n",
"# Path to output that will be generated with the following columns = File Name, exists, locations\n",
"output_csv = '../CNC/batch4/batch4_missing_files_2_paths_txt.csv'\n",
"# If copy = True, the files found will be copied over to this destination in s3 (User Keys needs copy permission)\n",
"destination_prefix = 'batch_4_priority_files/hotfix_diff_121124/TXT_FILES'\n",
"copy = True\n",
"# File Extension that will be added to the end of the file names - to look for\n",
"file_extension = '.txt'\n",
"\n",
"process_csv(input_csv, output_csv)\n",
"print(\"CSV processing completed.\")"
]
}
],
"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
}
@@ -3,6 +3,16 @@ import boto3
from concurrent.futures import ThreadPoolExecutor
from botocore.exceptions import ClientError
"""
This script is used to search for files in an S3 bucket and copy them to a new location.
This is used when CNC team comes back with a list of missing files and we need to search for them in the S3 bucket.
We usually run this script to first just search for the files without copying, then we analyze the results and if needed, we run the script again with copy=True.
"""
s3 = boto3.Session(profile_name='temp_cred').client('s3')
def list_all_files(bucket_name):
@@ -2,6 +2,16 @@ import boto3
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
"""
This script is used to search for files in a specific S3 bucket with a specific prefix and copy them to another S3 bucket with a different prefix.
This is similar to the script s3_search_and_copy.py, but this script is designed to be used with a CSV file that contains a list of file names to search for in the prefix to limit the scope.
"""
s3_client = boto3.Session(profile_name='temp_cred').client('s3')
def get_filenames_from_s3(bucket, prefix):
+10
View File
@@ -3,6 +3,16 @@ import re
import csv
from concurrent.futures import ThreadPoolExecutor
"""
This script reads all the text files in a specific S3 bucket and prefix, and extracts the last page number from each file.
The output is written to a CSV file with the filename and the last page number.
This script is not used often and was needed for an adhoc analysis. It is not part of the regular data pipeline.
"""
BUCKET_NAME = "centene-national-contracting-files"
PREFIX = "batch_1_priority_files/text_files/"
OUTPUT_CSV = "batch1/batch1_s3_files_page_counts.csv"
@@ -13,7 +13,7 @@ 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.
4. **Export Data**: Export the cost data and bucket record counts to CSV files. (This is currently disabled by default.)
"""
@@ -82,15 +82,13 @@ def process_bucket(bucket_name, start_date_obj, end_date_obj, profile_name):
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
end_date = '2024-11-30' # Replace with your end date
client_buckets = ['highmark-files', 'la-care-files',
'centene-fidelis-files',
'centene-healthnet-files',
@@ -148,6 +146,29 @@ def main():
total_costs = sum([cost['Cost'] for cost in cost_data])
cost_data.append({'Category': 'Total', 'Item': 'Total', 'Cost': total_costs})
# Adding up all costs that are not Amazon Textract and other costs as bedrock costs
# then based on the costs, we split the support and tax costs respective to the textract vs bedrock costs
textract_costs = service_costs.get('Amazon Textract', 0.0)
bedrock_costs = total_costs - textract_costs
support_costs_textract = support_costs.get('Support', 0.0) * (textract_costs / total_costs)
support_costs_bedrock = support_costs.get('Support', 0.0) * (bedrock_costs / total_costs)
tax_costs_textract = tax_costs.get('Tax', 0.0) * (textract_costs / total_costs)
tax_costs_bedrock = tax_costs.get('Tax', 0.0) * (bedrock_costs / total_costs)
print(f"Pro-rated Support + Tax costs for Textract based on total costs: ${support_costs_textract + tax_costs_textract:.2f}")
print(f"Pro-rated Support + Tax costs for Bedrock based on total costs: ${support_costs_bedrock + tax_costs_bedrock:.2f}")
total_textract_costs = textract_costs + support_costs_textract + tax_costs_textract
total_bedrock_costs = bedrock_costs + support_costs_bedrock + tax_costs_bedrock
print("\nAWS Service Costs pro-rated taxes by service :")
print(f" TOTAL Amazon Textract: ${total_textract_costs:.2f}")
print(f" TOTAL Amazon Bedrock: ${total_bedrock_costs:.2f}")
# Process each bucket and count records
print("\nBucket-wise Record Counts:")
bucket_data = []
@@ -156,18 +177,39 @@ def main():
print(f" {bucket}: {total_records} records")
bucket_data.append({'Bucket': bucket, 'Records': total_records})
# Combine all record counts for buckets and calcuate the percent of total for each bucket. Ingoring buckets with 0 records
total_records = sum([record['Records'] for record in bucket_data])
for record in bucket_data:
if record['Records'] > 0:
record['Percent of Total'] = (record['Records'] / total_records) * 100
# Printing the record weightage for each bucket
print("\nBucket-wise Record Weightage:")
for record in bucket_data:
if record['Records'] > 0:
print(f" {record['Bucket']}: {record['Percent of Total']:.2f}%")
# Calcualting the textract cost with respect to the percent of total
textract_cost_bucket = total_textract_costs * record['Percent of Total'] / 100
print(f" {record['Bucket']}: ${textract_cost_bucket:.2f}")
print(f'Total textract documents from buckets: {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'.")
# 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'.")
# bucket_df.to_csv('bucket_records.csv', index=False)
# print("Bucket record counts exported to 'bucket_records.csv'.")
if __name__ == '__main__':
main()
+283
View File
@@ -0,0 +1,283 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is an adhoc script that is used to ease the process of archiving. \n",
"Initially there was a scirpt that would run the process of archiving but there were edge cases where that script would fail due to the file names having nuances. \n",
"\n",
"This script simplifies that process. \n",
"It will take input of S3 URIs of folders that need to be archived, \n",
"e.g. \n",
"s3://centene-national-contracting-files/batch_3_priority_files/3b_missing_files_103024/\n",
"\n",
"And move it to \n",
"\n",
"s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/3b_missing_files_103024/\n",
"\n",
"This archived folders within the batch folder. \n",
"\n",
"There is an option for dryrun which can be set as true which only outputs the \"aws s3 mv --recursive\" command. \n",
" This can be used to run it manually to have more control over the folders that get archived \n",
"\n",
"Or we can set dry_run as False and let the subprocess run the aws cli command.\n",
"\n",
"This is setup as ipynb to ensure some more control over variables and paths and iterating accordingly "
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"import subprocess\n",
"import logging\n",
"import posixpath\n",
"import sys\n",
"\n",
"# ----------------------------- Configuration ----------------------------- #\n",
"\n",
"# List of S3 URIs to archive\n",
"S3_URIS = [\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/3b_missing_files_103024/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/dups_rerun_outputs/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/dups-for-textract/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/for-textract/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/hotfix_rerun_112724/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/redundant_dups/',\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/tables-for-textract/'\n",
" 's3://centene-national-contracting-files/batch_3_priority_files/throttled_files/'\n",
" # Add more S3 URIs as needed\n",
"]\n",
"\n",
"# AWS Profile to use\n",
"AWS_PROFILE = 'doczy_uat' # Replace with your AWS profile name\n",
"\n",
"# Dry Run Configuration\n",
"DRY_RUN = False # Set to False to execute the commands\n",
"\n",
"# Logging Configuration\n",
"LOG_FILE = 's3_mv_commands.log' # Log file name\n",
"\n",
"# ---------------------------- End Configuration --------------------------- #\n",
"\n",
"# Initialize logging\n",
"logging.basicConfig(\n",
" filename=LOG_FILE,\n",
" filemode='a',\n",
" format='%(asctime)s - %(levelname)s - %(message)s',\n",
" level=logging.INFO\n",
")\n",
"\n",
"# Initialize a thread-safe print function\n",
"from threading import Lock\n",
"print_lock = Lock()\n",
"\n",
"def log(message, level='info'):\n",
" \"\"\"\n",
" Logs messages to both the console and a log file.\n",
"\n",
" :param message: The message to log.\n",
" :param level: The logging level ('info', 'error', 'warning', 'debug').\n",
" \"\"\"\n",
" with print_lock:\n",
" if level == 'info':\n",
" logging.info(message)\n",
" print(message)\n",
" elif level == 'error':\n",
" logging.error(message)\n",
" print(f\"ERROR: {message}\")\n",
" elif level == 'warning':\n",
" logging.warning(message)\n",
" print(f\"WARNING: {message}\")\n",
" elif level == 'debug':\n",
" logging.debug(message)\n",
" print(f\"DEBUG: {message}\")\n",
"\n",
"def parse_s3_uri(s3_uri):\n",
" \"\"\"\n",
" Parses an S3 URI and returns the bucket and key.\n",
"\n",
" :param s3_uri: The S3 URI (e.g., s3://bucket/key)\n",
" :return: Tuple of (bucket, key)\n",
" \"\"\"\n",
" if not s3_uri.startswith('s3://'):\n",
" raise ValueError(f\"Invalid S3 URI: {s3_uri}\")\n",
" parts = s3_uri[5:].split('/', 1)\n",
" if len(parts) != 2:\n",
" raise ValueError(f\"Invalid S3 URI: {s3_uri}\")\n",
" bucket, key = parts\n",
" return bucket, key\n",
"\n",
"def construct_destination_key(bucket, key):\n",
" \"\"\"\n",
" Constructs the destination key by inserting 'ARCHIVE/' after the batch prefix.\n",
"\n",
" :param bucket: The S3 bucket name.\n",
" :param key: The original S3 object key.\n",
" :return: The destination S3 object key.\n",
" \"\"\"\n",
" # Split the key into parts\n",
" parts = key.split('/', 1)\n",
" if len(parts) == 1:\n",
" # If there's no subfolder, place ARCHIVE directly\n",
" archive_key = posixpath.join('ARCHIVE', parts[0])\n",
" else:\n",
" batch_prefix, sub_key = parts\n",
" archive_key = posixpath.join(batch_prefix, 'ARCHIVE', sub_key)\n",
" return archive_key\n",
"\n",
"def generate_mv_command(source_uri, destination_uri):\n",
" \"\"\"\n",
" Generates the aws s3 mv command.\n",
"\n",
" :param source_uri: Source S3 URI.\n",
" :param destination_uri: Destination S3 URI.\n",
" :return: The command as a list suitable for subprocess.\n",
" \"\"\"\n",
" command = [\n",
" 'aws',\n",
" 's3',\n",
" 'mv',\n",
" source_uri,\n",
" destination_uri,\n",
" '--profile',\n",
" AWS_PROFILE,\n",
" '--recursive'\n",
" ]\n",
" return command\n",
"\n",
"def execute_command(command):\n",
" \"\"\"\n",
" Executes a command using subprocess.\n",
"\n",
" :param command: The command as a list.\n",
" :return: Tuple of (returncode, stdout, stderr)\n",
" \"\"\"\n",
" try:\n",
" result = subprocess.run(\n",
" command,\n",
" check=False, # We'll handle errors manually\n",
" stdout=subprocess.PIPE,\n",
" stderr=subprocess.PIPE,\n",
" text=True\n",
" )\n",
" return result.returncode, result.stdout, result.stderr\n",
" except Exception as e:\n",
" return -1, '', str(e)\n",
"\n",
"def main():\n",
" \"\"\"\n",
" Main function to process the list of S3 URIs and perform mv operations.\n",
" \"\"\"\n",
" log(f\"Starting S3 mv operations with DRY_RUN={DRY_RUN}\", 'info')\n",
" \n",
" for s3_uri in S3_URIS:\n",
" try:\n",
" bucket, key = parse_s3_uri(s3_uri)\n",
" destination_key = construct_destination_key(bucket, key)\n",
" destination_uri = f\"s3://{bucket}/{destination_key}\"\n",
" \n",
" log(f\"Source URI: {s3_uri}\", 'info')\n",
" log(f\"Destination URI: {destination_uri}\", 'info')\n",
" \n",
" command = generate_mv_command(s3_uri, destination_uri)\n",
" command_str = ' '.join(command)\n",
" log(f\"Generated Command: {command_str}\", 'debug')\n",
" \n",
" if DRY_RUN:\n",
" log(f\"[Dry Run] Command to execute: {command_str}\", 'info')\n",
" continue # Skip execution in dry run mode\n",
" \n",
" # Execute the command\n",
" returncode, stdout, stderr = execute_command(command)\n",
" if returncode == 0:\n",
" log(f\"Successfully moved: {s3_uri} to {destination_uri}\", 'info')\n",
" else:\n",
" log(f\"Failed to move: {s3_uri} to {destination_uri}\", 'error')\n",
" log(f\"Error: {stderr.strip()}\", 'error')\n",
" \n",
" except Exception as e:\n",
" log(f\"Error processing URI '{s3_uri}': {e}\", 'error')\n",
" \n",
" log(\"S3 mv operations completed.\", 'info')\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Starting S3 mv operations with DRY_RUN=False\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/3b_missing_files_103024/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/3b_missing_files_103024/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/3b_missing_files_103024/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/3b_missing_files_103024/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/3b_missing_files_103024/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/3b_missing_files_103024/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/dups_rerun_outputs/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups_rerun_outputs/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/dups_rerun_outputs/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups_rerun_outputs/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/dups_rerun_outputs/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups_rerun_outputs/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/dups-for-textract/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups-for-textract/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/dups-for-textract/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups-for-textract/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/dups-for-textract/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/dups-for-textract/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/for-textract/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/for-textract/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/for-textract/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/for-textract/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/for-textract/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/for-textract/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/hotfix_rerun_112724/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/hotfix_rerun_112724/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/hotfix_rerun_112724/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/hotfix_rerun_112724/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/hotfix_rerun_112724/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/hotfix_rerun_112724/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/redundant_dups/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/redundant_dups/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/redundant_dups/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/redundant_dups/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/redundant_dups/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/redundant_dups/\n",
"Source URI: s3://centene-national-contracting-files/batch_3_priority_files/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/\n",
"Destination URI: s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/\n",
"DEBUG: Generated Command: aws s3 mv s3://centene-national-contracting-files/batch_3_priority_files/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/ s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/ --profile doczy_uat --recursive\n",
"Successfully moved: s3://centene-national-contracting-files/batch_3_priority_files/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/ to s3://centene-national-contracting-files/batch_3_priority_files/ARCHIVE/tables-for-textract/s3://centene-national-contracting-files/batch_3_priority_files/throttled_files/\n",
"S3 mv operations completed.\n"
]
}
],
"source": [
"main()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.10.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+83
View File
@@ -0,0 +1,83 @@
import boto3
import pandas as pd
from concurrent.futures import ThreadPoolExecutor
import os
import math
import csv
"""
This scirpt compares the file names in an Excel file (Batch excel files that MCS usually provides) with the file names in an S3 bucket.
The Excel file must contain a column with the file names to search for.
This is used mainly to find files that are missing in the batch staging files and cannot be located in the s3 bucket.
The output of this script can be then used with t_drive_search.py to find the missing files in the T drive.
Note: This script outputs both a CSV and an Excel file with the missing files and encoding issues.
We use the output excel file with the tdrive search script to find the missing files in the T drive.
"""
# Function to list S3 objects and normalize the filenames
def list_s3_files(bucket_name, prefix, s3_client):
normalized_s3_files = []
paginator = s3_client.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=bucket_name, Prefix=prefix):
if 'Contents' in page:
for obj in page['Contents']:
key = obj['Key']
# Extract and normalize the filename (remove prefix and extension)
# filename = key.split('/')[-1] # Take the file name from the key
filename = os.path.basename(key) # Remove any directory path
if len(filename) > 4:
normalized_filename = filename[:-4] # Remove the last 4 characters (file extension)
normalized_s3_files.append(normalized_filename)
return set(normalized_s3_files)
# Main function for processing
def find_missing_files(input_excel, bucket_name, prefix, output_csv, max_workers=10):
# Initialize boto3 session and S3 client
session = boto3.Session(profile_name='doczy_uat')
s3_client = session.client('s3')
# Read the Excel file with header=1
excel_df = pd.read_excel(input_excel, header=1, sheet_name='Sheet1')
excel_filenames = excel_df['File Name'] # Remove extensions if present
excel_filenames_set = set(excel_filenames)
# Get the normalized S3 filenames
normalized_s3_files = list_s3_files(bucket_name, prefix, s3_client)
# Find files that are in Excel but not in S3
missing_files = excel_filenames_set - normalized_s3_files
print(f"Type of missing_files: {type(missing_files)}")
print(f"Type of normalized_files: {type(normalized_s3_files)}")
print(f"Type of excel_filenames_set: {type(excel_filenames_set)}")
# Identify files that may have encoding differences, ensuring only strings are processed
missing_files_list = [file for file in missing_files if isinstance(file, str) and not (isinstance(file, float) and math.isnan(file))]
non_encoded_issues = [file for file in missing_files_list if all(ord(char) < 128 for char in file)]
output_df = pd.DataFrame({
'Missing File Name': missing_files_list,
'Encoding Check': ['No Encoding Issue' if file in non_encoded_issues else 'Encoding Issue' for file in missing_files_list]
})
output_df.to_csv(output_csv, index=False, encoding='utf-8', quoting=csv.QUOTE_ALL)
# Saving output as excel file
output_df.to_excel(output_csv[:-4] + '.xlsx', index=False)
# Write the missing files and non-encoded issue files to the output CSV
# with open(output_csv, mode='w', newline='', encoding='utf-8') as csvfile:
# csvfile.write('Missing File Name,Encoding Check\n')
# for file in missing_files_list:
# encoding_status = 'No Encoding Issue' if file in non_encoded_issues else 'Encoding Issue'
# csvfile.write(f"{file},{encoding_status}\n")
if __name__ == "__main__":
input_excel = 'C:\\Doczy\\National contracting\\Allbatches\\batch_files\\updated_batches_102424\\first_batch\\Batch 14_10222024.xlsx' # Path to your input Excel file
bucket_name = 'centene-national-contracting-files' # Your S3 bucket name
prefix = 'batch_14_priority_files/TXT_FILES/' # Your S3 prefix
output_csv = 'C:\\Doczy\\National contracting\\Allbatches\\diff_batch14_2.csv' # This will also save as xls
find_missing_files(input_excel, bucket_name, prefix, output_csv)
+187
View File
@@ -0,0 +1,187 @@
import os
import re
import pandas as pd
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial
from tqdm import tqdm
import logging
"""
This script searches for files in a directory based on a list of file names in an Excel file.
The Excel file must contain a column with the file names to search for.
This is used mainly to find files that are missing in the batch staging files and cannot be located in the s3 bucket.
Usually the flow of staging and prepping batches involve:
1. Staging the files in the s3 bucket
2. Running the excel_s3_diff.py script to compare the files in the s3 bucket with the files in the staging folder
3. The output of that script is an excel file that contains the missing files in the s3 bucket
4. This script is then used to search for the missing files in the staging folder
5. The output of this script is an excel file that contains the missing files in the staging folder
6. The output from this diff will have files names with encoding issue / no enocoding issue.
For files without any encoding issue usually are found in the t drive. We use the path to upoad it to a prefix in the s3 bucket for the client/batch_#
7. Then the staged pdfs are then processed with the scheduler to run the pdfs through the textract pipeline to extract the text from the pdfs.
"""
# --------------------------- Configuration ---------------------------
# Path to the Excel file
# EXCEL_FILE_PATH = "C:\\Doczy\\National contracting\\Allbatches\\diff_batch13.xlsx" # <-- Update this path
EXCEL_FILE_PATH = 'C:\\Doczy\\National contracting\\Allbatches\\new_diff_batch14_3_modified.xlsx'
# Directory to search within
SEARCH_DIRECTORY = 'T:\\AArete Client Work\\Doczy-Production\\Restricted\\2024-06-28-pdf' # <-- Update this path
# Output Excel file path
OUTPUT_FILE_PATH = 'C:\\Doczy\\National contracting\\Allbatches\\missing files analysis\\batch_14_search_output_3.xlsx' # <-- Update this path
# Maximum search depth
MAX_DEPTH = 5
# Number of threads for multi-threading
NUM_THREADS = 100 # Start with 4 and adjust as needed
# Log file path
LOG_FILE_PATH = 'C:\\Doczy\\National contracting\\Allbatches\\missing files analysis\\batch_14_search_3.log'
# --------------------------- Logging Setup ---------------------------
logging.basicConfig(
level=logging.INFO, # Change to DEBUG for more detailed logs
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_FILE_PATH),
logging.StreamHandler()
]
)
# --------------------------- Helper Functions ---------------------------
def remove_suffix(file_name):
"""
Removes suffixes like (1), (2), etc., from the file name.
Example: "XYZ_DOCUMENT(1).pdf" -> "XYZ_DOCUMENT.pdf"
"""
# Removing last 3 characters from the file name
cleaned_name = file_name[:-3].strip() if file_name.endswith(('(1)', '(2)', '(3)', '(4)')) else file_name
logging.debug(f"Removed suffix: '{file_name}' -> '{cleaned_name}'")
return cleaned_name
def find_files(search_dir, target_name, max_depth, file_types):
"""
Searches for files matching the target_name within search_dir up to max_depth.
Returns a list of full file paths.
"""
logging.info(f"Searching for '{target_name}' in '{search_dir}' with max depth {max_depth}")
results = []
target_name_lower = target_name.lower()
search_dir = Path(search_dir).resolve()
base_depth = len(search_dir.parts)
for root, dirs, files in os.walk(search_dir, topdown=True):
current_depth = len(Path(root).parts) - base_depth + 1
if current_depth > max_depth:
dirs[:] = [] # Prevent descending further
logging.debug(f"Reached max depth at: {root}")
continue
for file in files:
if Path(file).suffix.lower() in file_types:
if file.lower() == target_name_lower:
file_path = Path(root) / file
logging.info(f"Found file: {file_path}")
results.append(file_path)
logging.info(f"Search complete for '{target_name}'. Found {len(results)} file(s).")
return results
def search_file(row, search_dir, max_depth, file_types):
"""
Processes a single row to find the appropriate file path based on the Reason.
"""
# file_name = row['File Name']
# reason = row['Reason']
file_name = row['Missing File Name']
reason = row['Encoding Check']
found_path = None
# Remove suffix like (1), (2), etc.
cleaned_file_name = remove_suffix(file_name)
# Ensure the file has a .pdf extension
cleaned_file_name += '.pdf'
print(f"Cleaned file name: {cleaned_file_name}")
# Perform a case-insensitive search for the cleaned file name
found_files = find_files(search_dir, cleaned_file_name, max_depth, file_types)
if found_files:
# Select the file with the largest size if multiple are found
largest_file = max(found_files, key=lambda f: f.stat().st_size)
found_path = str(largest_file.resolve())
logging.debug(f"Selected largest file: {found_path}")
else:
logging.warning(f"File '{cleaned_file_name}' not found for Reason: '{reason}'")
return found_path
# --------------------------- Main Processing ---------------------------
def main():
logging.info("Script started.")
# Read the Excel file
try:
df = pd.read_excel(EXCEL_FILE_PATH, sheet_name='Sheet1', engine='openpyxl')
logging.info(f"Excel file '{EXCEL_FILE_PATH}' read successfully.")
except Exception as e:
logging.error(f"Error reading Excel file: {e}")
return
# Ensure required columns exist
# required_columns = {'File Name', 'Reason'}
required_columns = {'Missing File Name', 'Encoding Check'}
if not required_columns.issubset(df.columns):
logging.error(f"Error: The Excel sheet must contain the following columns: {required_columns}")
return
# Initialize a new column for found paths
df['Found Path'] = None
# Prepare for multi-threaded processing
file_types = {'.pdf'}
search_partial = partial(search_file, search_dir=SEARCH_DIRECTORY, max_depth=MAX_DEPTH, file_types=file_types)
with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
# Submit all tasks
futures = {executor.submit(search_partial, row): idx for idx, row in df.iterrows()}
# Iterate through completed futures with a progress bar
for future in tqdm(as_completed(futures), total=len(futures), desc="Processing"):
idx = futures[future]
try:
found_path = future.result()
df.at[idx, 'Found Path'] = found_path
logging.debug(f"Row {idx} processed. Found Path: {found_path}")
except Exception as e:
df.at[idx, 'Found Path'] = f"Error: {e}"
logging.error(f"Error processing row {idx}: {e}")
# Save the results to a new Excel file
try:
df.to_excel(OUTPUT_FILE_PATH, index=False)
logging.info(f"Processing complete. Results saved to '{OUTPUT_FILE_PATH}'")
except Exception as e:
logging.error(f"Error saving output Excel file: {e}")
logging.info("Script finished.")
if __name__ == "__main__":
main()