Spaces:
Sleeping
Sleeping
Upload 25 files
Browse files- README.md +28 -12
- __init__.py +0 -0
- app.py +139 -0
- config.yaml +18 -0
- core/__init__.py +10 -0
- core/analyzer.py +41 -0
- core/form_filler/background.js +10 -0
- core/form_filler/content.js +14 -0
- core/form_filler/manifest.json +17 -0
- core/form_filler/popup.html +10 -0
- core/optimizer.py +39 -0
- core/tracker.py +57 -0
- credentials.json +13 -0
- data_models/__init__.py +5 -0
- data_models/job_description.py +32 -0
- data_models/resume.py +23 -0
- requirement.txt +22 -0
- setup.py +16 -0
- templates/Ramen_DXC.docx +0 -0
- templates/professional.docx +0 -0
- utils/__init__.py +5 -0
- utils/config_manager.py +9 -0
- utils/file_handlers.py +69 -0
- utils/file_handlers1.py +55 -0
- utils/logger.py +18 -0
README.md
CHANGED
|
@@ -1,12 +1,28 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ATS Optimizer Pro
|
| 2 |
+
|
| 3 |
+
Complete solution to optimize resumes for applicant tracking systems.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
1. **Format-Preserving Analysis**
|
| 8 |
+
- Parses PDF/Word resumes while keeping original formatting
|
| 9 |
+
- Advanced section detection
|
| 10 |
+
|
| 11 |
+
2. **5-Factor ATS Scoring**
|
| 12 |
+
- Keyword matching (TF-IDF + exact match)
|
| 13 |
+
- Section completeness
|
| 14 |
+
- Semantic similarity (Qdrant vectors)
|
| 15 |
+
- Experience matching
|
| 16 |
+
- Education verification
|
| 17 |
+
|
| 18 |
+
3. **AI-Powered Optimization**
|
| 19 |
+
- DeepSeek API integration
|
| 20 |
+
- 3-step iterative refinement
|
| 21 |
+
- Format-aware rewriting
|
| 22 |
+
|
| 23 |
+
## Setup
|
| 24 |
+
|
| 25 |
+
1. Install requirements:
|
| 26 |
+
```bash
|
| 27 |
+
pip install -r requirements.txt
|
| 28 |
+
python -m spacy download en_core_web_md
|
__init__.py
ADDED
|
File without changes
|
app.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from ats_optimizer.core import ResumeAnalyzer, ResumeOptimizer
|
| 3 |
+
from ats_optimizer.data_models import Resume, JobDescription
|
| 4 |
+
from ats_optimizer.utils.file_handlers import FileHandler # Updated import
|
| 5 |
+
# from ats_optimizer.utils import FileHandler, Config
|
| 6 |
+
from ats_optimizer.utils.config_manager import Config
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import tempfile
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
# Fix module imports for Hugging Face
|
| 12 |
+
sys.path.append(str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
# def main():
|
| 15 |
+
# # Initialize components
|
| 16 |
+
# config = Config('config.yaml')
|
| 17 |
+
# analyzer = ResumeAnalyzer()
|
| 18 |
+
# optimizer = ResumeOptimizer(config.deepseek_api_key)
|
| 19 |
+
|
| 20 |
+
# # Streamlit UI
|
| 21 |
+
# st.title("🚀 ATS Optimizer Pro")
|
| 22 |
+
# st.markdown("Upload your resume and job description to analyze and optimize for ATS compatibility")
|
| 23 |
+
|
| 24 |
+
# # File upload section
|
| 25 |
+
# with st.expander("Upload Files", expanded=True):
|
| 26 |
+
# col1, col2 = st.columns(2)
|
| 27 |
+
# with col1:
|
| 28 |
+
# resume_file = st.file_uploader("Resume", type=["pdf", "docx"], key="resume_upload")
|
| 29 |
+
# with col2:
|
| 30 |
+
# jd_file = st.file_uploader("Job Description", type=["pdf", "docx", "txt"], key="jd_upload")
|
| 31 |
+
|
| 32 |
+
# if st.button("Analyze", type="primary"):
|
| 33 |
+
# if resume_file and jd_file:
|
| 34 |
+
# with st.spinner("Processing files..."):
|
| 35 |
+
# try:
|
| 36 |
+
# # Save uploaded files
|
| 37 |
+
# with tempfile.TemporaryDirectory() as temp_dir:
|
| 38 |
+
# resume_path = FileHandler.save_uploaded_file(resume_file, temp_dir)
|
| 39 |
+
# jd_path = FileHandler.save_uploaded_file(jd_file, temp_dir)
|
| 40 |
+
|
| 41 |
+
# if not resume_path or not jd_path:
|
| 42 |
+
# st.error("Failed to process uploaded files")
|
| 43 |
+
# return
|
| 44 |
+
|
| 45 |
+
# # Analyze documents
|
| 46 |
+
# resume = analyzer.parse_resume(resume_path)
|
| 47 |
+
# jd = analyzer.parse_jd(jd_path)
|
| 48 |
+
|
| 49 |
+
# # Calculate score
|
| 50 |
+
# score = analyzer.calculate_ats_score(resume, jd)
|
| 51 |
+
|
| 52 |
+
# # Display results
|
| 53 |
+
# st.subheader("📊 Analysis Results")
|
| 54 |
+
# st.metric("Overall ATS Score", f"{score['overall_score']:.1f}%")
|
| 55 |
+
|
| 56 |
+
# with st.expander("Detailed Scores"):
|
| 57 |
+
# st.write(f"Keyword Match: {score['keyword_score']:.1f}%")
|
| 58 |
+
# st.write(f"Section Completeness: {score['section_score']:.1f}%")
|
| 59 |
+
# st.write(f"Experience Match: {score['experience_score']:.1f}%")
|
| 60 |
+
|
| 61 |
+
# # Optimization section
|
| 62 |
+
# st.subheader("🛠 Optimization")
|
| 63 |
+
# if st.button("Optimize Resume", key="optimize_btn"):
|
| 64 |
+
# with st.spinner("Rewriting resume..."):
|
| 65 |
+
# optimized = optimizer.rewrite_resume(resume, jd)
|
| 66 |
+
# temp_output = Path(temp_dir) / "optimized_resume.docx"
|
| 67 |
+
# FileHandler.save_resume(optimized, str(temp_output))
|
| 68 |
+
|
| 69 |
+
# st.success("Optimization complete!")
|
| 70 |
+
# with open(temp_output, "rb") as f:
|
| 71 |
+
# st.download_button(
|
| 72 |
+
# "Download Optimized Resume",
|
| 73 |
+
# data=f,
|
| 74 |
+
# file_name="optimized_resume.docx",
|
| 75 |
+
# mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 76 |
+
# )
|
| 77 |
+
|
| 78 |
+
# except Exception as e:
|
| 79 |
+
# st.error(f"An error occurred: {str(e)}")
|
| 80 |
+
# st.stop()
|
| 81 |
+
# else:
|
| 82 |
+
# st.warning("Please upload both resume and job description files")
|
| 83 |
+
|
| 84 |
+
# if __name__ == "__main__":
|
| 85 |
+
# main()
|
| 86 |
+
|
| 87 |
+
def main():
|
| 88 |
+
# Initialize components
|
| 89 |
+
config = Config('config.yaml')
|
| 90 |
+
analyzer = ResumeAnalyzer()
|
| 91 |
+
optimizer = ResumeOptimizer(config.deepseek_api_key)
|
| 92 |
+
|
| 93 |
+
# Streamlit UI
|
| 94 |
+
st.title("ATS Optimizer Pro")
|
| 95 |
+
|
| 96 |
+
# File upload
|
| 97 |
+
resume_file = st.file_uploader("Upload Resume", type=["pdf", "docx"])
|
| 98 |
+
jd_file = st.file_uploader("Upload Job Description", type=["pdf", "docx", "txt"])
|
| 99 |
+
|
| 100 |
+
if st.button("Analyze"):
|
| 101 |
+
if resume_file and jd_file:
|
| 102 |
+
with st.spinner("Processing..."):
|
| 103 |
+
try:
|
| 104 |
+
# Create temp directory
|
| 105 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 106 |
+
# Save uploaded files
|
| 107 |
+
resume_path = FileHandler.save_uploaded_file(resume_file, temp_dir)
|
| 108 |
+
jd_path = FileHandler.save_uploaded_file(jd_file, temp_dir)
|
| 109 |
+
|
| 110 |
+
if not resume_path or not jd_path:
|
| 111 |
+
st.error("Failed to process uploaded files")
|
| 112 |
+
return
|
| 113 |
+
|
| 114 |
+
# Analyze documents
|
| 115 |
+
resume = analyzer.parse_resume(resume_path)
|
| 116 |
+
jd = analyzer.parse_jd(jd_path)
|
| 117 |
+
|
| 118 |
+
# Calculate score
|
| 119 |
+
score = analyzer.calculate_ats_score(resume, jd)
|
| 120 |
+
|
| 121 |
+
# Display results
|
| 122 |
+
st.subheader("Analysis Results")
|
| 123 |
+
st.json(score)
|
| 124 |
+
|
| 125 |
+
# Optimization
|
| 126 |
+
if st.button("Optimize Resume"):
|
| 127 |
+
optimized = optimizer.rewrite_resume(resume, jd)
|
| 128 |
+
output_path = os.path.join(temp_dir, "optimized_resume.docx")
|
| 129 |
+
if FileHandler.save_resume(optimized, output_path):
|
| 130 |
+
with open(output_path, "rb") as f:
|
| 131 |
+
st.download_button(
|
| 132 |
+
"Download Optimized Resume",
|
| 133 |
+
data=f,
|
| 134 |
+
file_name="optimized_resume.docx"
|
| 135 |
+
)
|
| 136 |
+
except Exception as e:
|
| 137 |
+
st.error(f"An error occurred: {str(e)}")
|
| 138 |
+
else:
|
| 139 |
+
st.warning("Please upload both resume and job description files")
|
config.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
deepseek:
|
| 2 |
+
api_key: "sk-cf0b89ca56e44cb9980113909ebe687e"
|
| 3 |
+
model: "deepseek-chat"
|
| 4 |
+
temperature: 0.7
|
| 5 |
+
|
| 6 |
+
google_sheets:
|
| 7 |
+
enabled: true # Set to true if using Google Sheets or else false
|
| 8 |
+
credentials: "credentials.json"
|
| 9 |
+
# sheet_id: "your-sheet-id"
|
| 10 |
+
sheet_name: "Job Applications"
|
| 11 |
+
|
| 12 |
+
scoring:
|
| 13 |
+
weights:
|
| 14 |
+
keyword: 0.3
|
| 15 |
+
section: 0.2
|
| 16 |
+
vector: 0.25
|
| 17 |
+
experience: 0.15
|
| 18 |
+
education: 0.1
|
core/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from .analyzer import ResumeAnalyzer
|
| 2 |
+
# from .optimizer import ResumeOptimizer
|
| 3 |
+
# from .tracker import ApplicationTracker
|
| 4 |
+
|
| 5 |
+
from ats_optimizer.core.analyzer import ResumeAnalyzer
|
| 6 |
+
from ats_optimizer.core.optimizer import ResumeOptimizer
|
| 7 |
+
from ats_optimizer.core.tracker import ApplicationTracker
|
| 8 |
+
|
| 9 |
+
__all__ = ['ResumeAnalyzer', 'ResumeOptimizer', 'ApplicationTracker']
|
| 10 |
+
# __all__ = ['ResumeAnalyzer', 'ResumeOptimizer'] #if doesnt want to use tracker & comment above import too
|
core/analyzer.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from qdrant_client import QdrantClient
|
| 2 |
+
from ats_optimizer.data_models.resume import Resume
|
| 3 |
+
from ats_optimizer.data_models.job_description import JobDescription
|
| 4 |
+
# from utils.logger import logger
|
| 5 |
+
# from ..utils.logger import logger
|
| 6 |
+
from ats_optimizer.utils.logger import logger
|
| 7 |
+
|
| 8 |
+
class ResumeAnalyzer:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.client = QdrantClient(":memory:")
|
| 11 |
+
self.client.set_model("BAAI/bge-base-en")
|
| 12 |
+
|
| 13 |
+
def parse_resume(self, file_path: str) -> Resume:
|
| 14 |
+
"""Step 1: Parse resume with formatting preservation"""
|
| 15 |
+
# Uses python-docx for Word docs, pdfminer for PDFs
|
| 16 |
+
raw_text, formatting = FileHandler.extract_with_formatting(file_path)
|
| 17 |
+
return Resume(raw_text, formatting)
|
| 18 |
+
|
| 19 |
+
def parse_jd(self, file_path: str) -> JobDescription:
|
| 20 |
+
"""Step 2: Analyze job description"""
|
| 21 |
+
raw_text = FileHandler.extract_text(file_path)
|
| 22 |
+
return JobDescription(raw_text)
|
| 23 |
+
|
| 24 |
+
def calculate_ats_score(self, resume: Resume, jd: JobDescription) -> dict:
|
| 25 |
+
"""Step 3: Comprehensive ATS scoring"""
|
| 26 |
+
# Enhanced scoring with 5 factors
|
| 27 |
+
scores = {
|
| 28 |
+
'keyword': self._keyword_match_score(resume, jd),
|
| 29 |
+
'section': self._section_completeness(resume, jd),
|
| 30 |
+
'vector': self._vector_similarity(resume, jd),
|
| 31 |
+
'experience': self._experience_match(resume, jd),
|
| 32 |
+
'education': self._education_match(resume, jd)
|
| 33 |
+
}
|
| 34 |
+
scores['overall'] = sum(w * s for w, s in [
|
| 35 |
+
(0.3, scores['keyword']),
|
| 36 |
+
(0.2, scores['section']),
|
| 37 |
+
(0.25, scores['vector']),
|
| 38 |
+
(0.15, scores['experience']),
|
| 39 |
+
(0.1, scores['education'])
|
| 40 |
+
])
|
| 41 |
+
return scores
|
core/form_filler/background.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
| 2 |
+
if (request.action === "fillForm") {
|
| 3 |
+
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
| 4 |
+
chrome.tabs.sendMessage(tabs[0].id, {
|
| 5 |
+
action: "fillFields",
|
| 6 |
+
resumeData: request.data
|
| 7 |
+
});
|
| 8 |
+
});
|
| 9 |
+
}
|
| 10 |
+
});
|
core/form_filler/content.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 2 |
+
if (msg.action === "fillFields") {
|
| 3 |
+
const fields = {
|
| 4 |
+
'input[name*="name"]': msg.resumeData.name,
|
| 5 |
+
'input[name*="email"]': msg.resumeData.email,
|
| 6 |
+
'textarea[name*="experience"]': msg.resumeData.experience
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
Object.entries(fields).forEach(([selector, value]) => {
|
| 10 |
+
const el = document.querySelector(selector);
|
| 11 |
+
if (el) el.value = value;
|
| 12 |
+
});
|
| 13 |
+
}
|
| 14 |
+
});
|
core/form_filler/manifest.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"manifest_version": 3,
|
| 3 |
+
"name": "ATS AutoFill",
|
| 4 |
+
"version": "1.0",
|
| 5 |
+
"permissions": ["activeTab", "storage"],
|
| 6 |
+
"action": {
|
| 7 |
+
"default_popup": "popup.html",
|
| 8 |
+
"default_icon": "icon.png"
|
| 9 |
+
},
|
| 10 |
+
"background": {
|
| 11 |
+
"service_worker": "background.js"
|
| 12 |
+
},
|
| 13 |
+
"content_scripts": [{
|
| 14 |
+
"matches": ["*://*/*"],
|
| 15 |
+
"js": ["content.js"]
|
| 16 |
+
}]
|
| 17 |
+
}
|
core/form_filler/popup.html
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>ATS AutoFill</title>
|
| 5 |
+
<script src="popup.js"></script>
|
| 6 |
+
</head>
|
| 7 |
+
<body>
|
| 8 |
+
<button id="fillButton">Fill Form</button>
|
| 9 |
+
</body>
|
| 10 |
+
</html>
|
core/optimizer.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from docx import Document
|
| 3 |
+
from data_models.resume import Resume
|
| 4 |
+
|
| 5 |
+
from ats_optimizer.data_models.job_description import JobDescription
|
| 6 |
+
from ats_optimizer.data_models.resume import Resume
|
| 7 |
+
|
| 8 |
+
class ResumeOptimizer:
|
| 9 |
+
def __init__(self, api_key: str):
|
| 10 |
+
self.api_key = api_key
|
| 11 |
+
# self.template = "templates/professional.docx"
|
| 12 |
+
self.template = "templates/Ramen_DXC.docx"
|
| 13 |
+
|
| 14 |
+
def rewrite_resume(self, resume: Resume, jd: JobDescription) -> Resume:
|
| 15 |
+
"""Step 4: AI rewriting with formatting preservation"""
|
| 16 |
+
prompt = self._build_optimization_prompt(resume, jd)
|
| 17 |
+
|
| 18 |
+
response = requests.post(
|
| 19 |
+
"https://api.deepseek.com/v1/chat/completions",
|
| 20 |
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
| 21 |
+
json={
|
| 22 |
+
"model": "deepseek-chat",
|
| 23 |
+
"messages": [{
|
| 24 |
+
"role": "user",
|
| 25 |
+
"content": prompt
|
| 26 |
+
}],
|
| 27 |
+
"temperature": 0.7
|
| 28 |
+
}
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Apply optimized content to original format
|
| 32 |
+
optimized_content = response.json()["choices"][0]["message"]["content"]
|
| 33 |
+
return self._apply_formatting(resume, optimized_content)
|
| 34 |
+
|
| 35 |
+
def _apply_formatting(self, original: Resume, new_content: str) -> Resume:
|
| 36 |
+
"""Preserve original formatting with new content"""
|
| 37 |
+
doc = Document(original.file_path)
|
| 38 |
+
# Advanced formatting preservation logic
|
| 39 |
+
return Resume.from_docx(doc, new_content)
|
core/tracker.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gspread
|
| 2 |
+
from oauth2client.service_account import ServiceAccountCredentials
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
'''
|
| 9 |
+
class ApplicationTracker:
|
| 10 |
+
def __init__(self, creds_path: str):
|
| 11 |
+
self.scope = ["https://www.googleapis.com/auth/spreadsheets"]
|
| 12 |
+
self.creds = ServiceAccountCredentials.from_json_keyfile_name(creds_path, self.scope)
|
| 13 |
+
|
| 14 |
+
def track(self, application_data: dict):
|
| 15 |
+
"""Track application in Google Sheets with enhanced fields"""
|
| 16 |
+
client = gspread.authorize(self.creds)
|
| 17 |
+
sheet = client.open("Job Applications").sheet1
|
| 18 |
+
|
| 19 |
+
sheet.append_row([
|
| 20 |
+
application_data['company'],
|
| 21 |
+
application_data['position'],
|
| 22 |
+
application_data['date'],
|
| 23 |
+
application_data['status'],
|
| 24 |
+
application_data['score'],
|
| 25 |
+
application_data['url'],
|
| 26 |
+
application_data['resume_version'],
|
| 27 |
+
application_data['notes']
|
| 28 |
+
])
|
| 29 |
+
'''
|
| 30 |
+
class ApplicationTracker:
|
| 31 |
+
def __init__(self, creds_path: str):
|
| 32 |
+
if not Path(creds_path).exists():
|
| 33 |
+
raise FileNotFoundError(f"Credentials file not found at {creds_path}")
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
self.scope = ["https://www.googleapis.com/auth/spreadsheets"]
|
| 37 |
+
self.creds = ServiceAccountCredentials.from_json_keyfile_name(creds_path, self.scope)
|
| 38 |
+
self.client = gspread.authorize(self.creds)
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"Google Sheets auth failed: {e}")
|
| 41 |
+
raise
|
| 42 |
+
|
| 43 |
+
def track(self, application_data: dict, sheet_name="Job Applications"):
|
| 44 |
+
try:
|
| 45 |
+
sheet = self.client.open(sheet_name).sheet1
|
| 46 |
+
sheet.append_row([
|
| 47 |
+
application_data.get('company', ''),
|
| 48 |
+
application_data.get('position', ''),
|
| 49 |
+
application_data.get('date_applied', ''),
|
| 50 |
+
application_data.get('status', 'Applied'),
|
| 51 |
+
application_data.get('score', ''),
|
| 52 |
+
application_data.get('url', '')
|
| 53 |
+
])
|
| 54 |
+
return True
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.error(f"Tracking failed: {e}")
|
| 57 |
+
return False
|
credentials.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "formal-theater-458022-d2",
|
| 4 |
+
"private_key_id": "e7494ef90b6d43b0dc15ce9f9db0922225a48694",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDGjEU29ETZSyx\nqr8zakizg+q3IqWq3pQdMVHH72GLq/vugkZiqao11YdFK3igyKIRApacpLAACjmm\nEoUwcKEh1R7WKExbAvyFTLIm65j9wZ2Z2LeAuRRZFjU2uQ+iCXOTQA7YgIPU3sMg\nLI+cMAITr0C4hLRnjgUcPeNQRG8M/Io3Oq4KeTDvEKS99aqVvA7zgGH98awdBd2c\ncciYGHaTUq5sJxK72AKFnPxrLHgbq9hiuc5fw7YdCVzmuVY/LXkwSkosbIsIrxUx\nPHwlnYdoWHZ0aI76x3qluw+MUqZoIEp3ctkIfHaZbLf4yuqTcbUuQ7RtkPFLifge\ndFsM7BnhAgMBAAECggEASLQr8h/wC5A6VYLReXFz4iGYh+JLZh9HhpFobl8QNKJE\nYZ7+Z6neGe2WWPpYG2JosnoKchkU1Q76aJ6iL2jpQthOg3PE8G1ueKYaBVLqUjWi\na0BNMZTGtmQGNHxGDRYEkazfW2KYvey9PfIdGhDx1TALqDcbmzNbSCjv2muGDoou\nzmEcAzdil5M0UNVctqxqAOk4PK3l59dSUdRgJLP6LsceT1xqNDw9Ps9x8vOUY3iq\n0kkOeXu+eTVIkgTxIsJGnBQ0wr+Ao5r12zJ8Y/iyXDVN/RTOl+aakegYiIYjeVZu\nEPyZ2fMNnQCtfSGzoLrcHyFxkhh1Fl2E4ZhW4hWh+QKBgQDgjtlnhBdhNuBGHOp0\nknRKWlhtRXzuIDklCmIS/vK0ZXpCF2wacXo7UKHQ7oVPsA5+0MqeV9teu8hqQm/Y\nZRipHU911zlsR81ZejR4WQ8xDci4Ewpt5JVkx+DfPlNXuPapgGTtg/KBDfKHWnnv\nNr/BSeRSNb5iSPkS/j4ENnrhEwKBgQDea4U0cy9J2YvBhOmb/Bk9fOIBcnlz/B2v\nWt/+ddIZnW1IOYgSd/W8W3GyQ803OzzuHa+y0vLNnGBwapOMPJ8foagy36XYbRxr\nSu8He+oTTJTgwidEnUz1DS8B3IIgi+FJK8YLUL8hVupuBqqI/r9G/wtULTFutzvD\n5/N/rCaruwKBgADOIlNvstHDa5x0wBZ46/fUSRrjM+Z6sRnD5sQgq+gfsQeJo/aY\nT5Lk4B+qq0m03OhxgTh+Iig9ziMrZ9FD04nPtBg9FFSiEUdv275Ou3I2lXCriM8K\nEcsRuGm0hIH9BM1oy3PalEUIMsVvep5z+M4NoMb2sF8T2ejKhphnRZuHAoGAL+rM\nFMOn8WoLwNJIndFPAr8v1Y36+nDbWFbkoOZzMA+JZqD2Xrw3VbABq50NzhNWChqd\nKpJlusQwxqc/SFwbD+581RD3osvG7pqDKoKYqDW8cTuCyDZ3SOfhM6503lwkWeYz\nUWbA9obKFJAdF0yCmuIBZ84gszCIkKkc/WlyH1cCgYEAtkmceBmt/YCPnaVDhk/L\n4hWqcVHqD2RU4DPPhjIXppjahDrcOzjD7Lt9swKK3QZEE840DcIAKVU140V2Neie\nOXqzgrLn/dIe0j15FK4lmY9GhCVU3kTx08JwxEiBIDoi0Z6F926C9dvjGxarpsi7\n77pNNpxzZ9i6mx9dK9ECuMI=\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "ats-optimizer@formal-theater-458022-d2.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "102212927624577642135",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ats-optimizer%40formal-theater-458022-d2.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
data_models/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from .resume import Resume
|
| 2 |
+
from .resume import Resume
|
| 3 |
+
from .job_description import JobDescription
|
| 4 |
+
|
| 5 |
+
__all__ = ['Resume', 'JobDescription']
|
data_models/job_description.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class JobDescription:
|
| 6 |
+
# raw_text: str
|
| 7 |
+
# extracted_keywords: List[str]
|
| 8 |
+
# keyterms: List[tuple]
|
| 9 |
+
# entities: List[str]
|
| 10 |
+
raw_text: str
|
| 11 |
+
extracted_keywords: list
|
| 12 |
+
keyterms: list
|
| 13 |
+
entities: list
|
| 14 |
+
|
| 15 |
+
@property
|
| 16 |
+
def required_skills(self) -> List[str]:
|
| 17 |
+
return [kw for kw in self.extracted_keywords if 'skill' in kw.lower()]
|
| 18 |
+
|
| 19 |
+
@property
|
| 20 |
+
def experience_requirements(self) -> Dict:
|
| 21 |
+
return {
|
| 22 |
+
'years': self._extract_years(),
|
| 23 |
+
'technologies': self._extract_tech()
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def _extract_years(self) -> int:
|
| 27 |
+
# Extract years requirement using regex
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
def _extract_tech(self) -> List[str]:
|
| 31 |
+
# Extract required technologies
|
| 32 |
+
pass
|
data_models/resume.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class Resume:
|
| 6 |
+
# raw_text: str
|
| 7 |
+
# formatting: Dict
|
| 8 |
+
# metadata: Dict
|
| 9 |
+
# scores: Dict = None
|
| 10 |
+
raw_text: str
|
| 11 |
+
extracted_keywords: list
|
| 12 |
+
keyterms: list
|
| 13 |
+
entities: list
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def from_docx(cls, file_path: str):
|
| 17 |
+
"""Parse while preserving formatting"""
|
| 18 |
+
text, formatting = parse_docx_with_formatting(file_path)
|
| 19 |
+
return cls(text, formatting, extract_metadata(text))
|
| 20 |
+
|
| 21 |
+
def to_docx(self, output_path: str):
|
| 22 |
+
"""Export with original formatting"""
|
| 23 |
+
apply_formatting(self.raw_text, self.formatting, output_path)
|
requirement.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core
|
| 2 |
+
python-docx==0.8.11
|
| 3 |
+
pdfminer.six==20221105
|
| 4 |
+
#python-docx-template==0.16.0
|
| 5 |
+
docxtpl==0.16.0
|
| 6 |
+
pdf2docx==0.5.6
|
| 7 |
+
qdrant-client==1.6.4
|
| 8 |
+
sentence-transformers==2.2.2
|
| 9 |
+
|
| 10 |
+
# API & Services
|
| 11 |
+
google-api-python-client==2.104.0
|
| 12 |
+
gspread==5.11.3
|
| 13 |
+
requests==2.31.0
|
| 14 |
+
oauth2client==4.1.3
|
| 15 |
+
|
| 16 |
+
# UI
|
| 17 |
+
streamlit==1.29.0
|
| 18 |
+
plotly==5.18.0
|
| 19 |
+
|
| 20 |
+
# NLP
|
| 21 |
+
spacy==3.7.2
|
| 22 |
+
en-core-web-md @ https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.7.0/en_core_web_md-3.7.0-py3-none-any.whl
|
setup.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# setup.py
|
| 2 |
+
from setuptools import setup, find_packages
|
| 3 |
+
|
| 4 |
+
setup(
|
| 5 |
+
name="ats_optimizer",
|
| 6 |
+
version="0.1",
|
| 7 |
+
packages=find_packages(),
|
| 8 |
+
install_requires=[
|
| 9 |
+
'python-docx>=0.8.11',
|
| 10 |
+
'pdfminer.six>=20221105',
|
| 11 |
+
'pyyaml>=6.0',
|
| 12 |
+
'streamlit>=1.28.0',
|
| 13 |
+
'spacy>=3.7.2',
|
| 14 |
+
],
|
| 15 |
+
python_requires='>=3.9',
|
| 16 |
+
)
|
templates/Ramen_DXC.docx
ADDED
|
Binary file (39.9 kB). View file
|
|
|
templates/professional.docx
ADDED
|
File without changes
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .file_handlers import FileHandler
|
| 2 |
+
from .logger import setup_logger
|
| 3 |
+
from .config_manager import Config
|
| 4 |
+
|
| 5 |
+
__all__ = ['FileHandler', 'setup_logger', 'Config']
|
utils/config_manager.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
|
| 3 |
+
class Config:
|
| 4 |
+
def __init__(self, config_path: str):
|
| 5 |
+
with open(config_path, 'r') as f:
|
| 6 |
+
self.config = yaml.safe_load(f)
|
| 7 |
+
|
| 8 |
+
def __getattr__(self, name):
|
| 9 |
+
return self.config.get(name)
|
utils/file_handlers.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from docx import Document
|
| 3 |
+
from pdfminer.high_level import extract_text
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from tempfile import NamedTemporaryFile
|
| 6 |
+
import uuid
|
| 7 |
+
|
| 8 |
+
class FileHandler:
|
| 9 |
+
@staticmethod
|
| 10 |
+
def read_file(file_path: str) -> str:
|
| 11 |
+
if file_path.endswith('.docx'):
|
| 12 |
+
return FileHandler._read_docx(file_path)
|
| 13 |
+
elif file_path.endswith('.pdf'):
|
| 14 |
+
return extract_text(file_path)
|
| 15 |
+
else:
|
| 16 |
+
with open(file_path, 'r') as f:
|
| 17 |
+
return f.read()
|
| 18 |
+
#--
|
| 19 |
+
|
| 20 |
+
@staticmethod
|
| 21 |
+
def save_uploaded_file(uploaded_file, directory="temp_uploads"):
|
| 22 |
+
"""Save Streamlit uploaded file to a temporary directory"""
|
| 23 |
+
try:
|
| 24 |
+
# Create directory if it doesn't exist
|
| 25 |
+
Path(directory).mkdir(exist_ok=True)
|
| 26 |
+
|
| 27 |
+
# Generate unique filename
|
| 28 |
+
file_ext = Path(uploaded_file.name).suffix
|
| 29 |
+
unique_id = uuid.uuid4().hex
|
| 30 |
+
temp_file = Path(directory) / f"{unique_id}{file_ext}"
|
| 31 |
+
|
| 32 |
+
# Save file
|
| 33 |
+
with open(temp_file, "wb") as f:
|
| 34 |
+
f.write(uploaded_file.getbuffer())
|
| 35 |
+
|
| 36 |
+
return str(temp_file)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"Error saving file: {e}")
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
@staticmethod
|
| 42 |
+
def cleanup_temp_files(directory="temp_uploads"):
|
| 43 |
+
"""Remove temporary files"""
|
| 44 |
+
try:
|
| 45 |
+
for file in Path(directory).glob("*"):
|
| 46 |
+
file.unlink()
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Error cleaning files: {e}")
|
| 49 |
+
#--
|
| 50 |
+
|
| 51 |
+
@staticmethod
|
| 52 |
+
def _read_docx(file_path: str) -> str:
|
| 53 |
+
doc = Document(file_path)
|
| 54 |
+
return '\n'.join([para.text for para in doc.paragraphs])
|
| 55 |
+
|
| 56 |
+
@staticmethod
|
| 57 |
+
def save_resume(resume_data: dict, output_path: str):
|
| 58 |
+
if output_path.endswith('.docx'):
|
| 59 |
+
FileHandler._save_as_docx(resume_data, output_path)
|
| 60 |
+
else:
|
| 61 |
+
with open(output_path, 'w') as f:
|
| 62 |
+
f.write(resume_data['content'])
|
| 63 |
+
|
| 64 |
+
@staticmethod
|
| 65 |
+
def _save_as_docx(resume_data: dict, output_path: str):
|
| 66 |
+
doc = Document()
|
| 67 |
+
# Add formatting preservation logic here
|
| 68 |
+
doc.save(output_path)
|
| 69 |
+
|
utils/file_handlers1.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import uuid
|
| 4 |
+
from docx import Document
|
| 5 |
+
from pdfminer.high_level import extract_text
|
| 6 |
+
|
| 7 |
+
class FileHandler:
|
| 8 |
+
@staticmethod
|
| 9 |
+
def save_uploaded_file(uploaded_file, directory="temp_uploads"):
|
| 10 |
+
"""Save Streamlit uploaded file to temporary directory"""
|
| 11 |
+
try:
|
| 12 |
+
# Create directory if needed
|
| 13 |
+
Path(directory).mkdir(exist_ok=True)
|
| 14 |
+
|
| 15 |
+
# Generate unique filename
|
| 16 |
+
file_ext = Path(uploaded_file.name).suffix
|
| 17 |
+
unique_id = uuid.uuid4().hex
|
| 18 |
+
temp_file = Path(directory) / f"{unique_id}{file_ext}"
|
| 19 |
+
|
| 20 |
+
# Save file
|
| 21 |
+
with open(temp_file, "wb") as f:
|
| 22 |
+
f.write(uploaded_file.getbuffer())
|
| 23 |
+
|
| 24 |
+
return str(temp_file)
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"Error saving file: {e}")
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
@staticmethod
|
| 30 |
+
def save_resume(resume_data, output_path):
|
| 31 |
+
"""Save resume content to file"""
|
| 32 |
+
try:
|
| 33 |
+
if output_path.endswith('.docx'):
|
| 34 |
+
doc = Document()
|
| 35 |
+
for paragraph in resume_data.split('\n'):
|
| 36 |
+
doc.add_paragraph(paragraph)
|
| 37 |
+
doc.save(output_path)
|
| 38 |
+
else:
|
| 39 |
+
with open(output_path, 'w') as f:
|
| 40 |
+
f.write(resume_data)
|
| 41 |
+
return True
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"Error saving resume: {e}")
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def cleanup_temp_files(directory="temp_uploads"):
|
| 48 |
+
"""Remove temporary files"""
|
| 49 |
+
try:
|
| 50 |
+
for file in Path(directory).glob("*"):
|
| 51 |
+
file.unlink()
|
| 52 |
+
return True
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Error cleaning files: {e}")
|
| 55 |
+
return False
|
utils/logger.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
def setup_logger(name: str = 'ats_optimizer'):
|
| 4 |
+
logger = logging.getLogger(name)
|
| 5 |
+
logger.setLevel(logging.INFO)
|
| 6 |
+
|
| 7 |
+
handler = logging.StreamHandler()
|
| 8 |
+
formatter = logging.Formatter(
|
| 9 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 10 |
+
)
|
| 11 |
+
handler.setFormatter(formatter)
|
| 12 |
+
logger.addHandler(handler)
|
| 13 |
+
|
| 14 |
+
return logger
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Create a module-level logger instance
|
| 18 |
+
logger = setup_logger()
|