Imarticuslearning commited on
Commit
8edfc76
Β·
verified Β·
1 Parent(s): 2e96629

Upload 6 files

Browse files
Files changed (6) hide show
  1. src/config.py +223 -0
  2. src/logging_config.py +42 -0
  3. src/main.py +61 -0
  4. src/resume_parser.py +1208 -0
  5. src/reviewer.py +84 -0
  6. src/submitter.py +32 -0
src/config.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ BASE_DIR = os.getcwd()
4
+ UPLOAD_FOLDER = os.path.join(BASE_DIR, '..', 'data', 'uploads')
5
+ ALLOWED_EXTENSIONS = {'pdf'}
6
+ linkedin_domain = (r'https?://(www\.)?linkedin\.com/[^\s<>"]')
7
+ github_domain = (r'https?://(www\.)?github\.com/[^\s<>"]')
8
+ kaggle_domain = (r'https?://(www\.)?kaggle\.com/[^\s<>"]')
9
+ medium_domain = (r'https?://(www\.)?medium\.com/[^\s<>"]')
10
+ hackerrank_domain = (r'https?://(www\.)?hackerrank\.com/[^\s<>"]')
11
+ leetcode_domain = (r'https?://(www\.)?leetcode\.com/[^\s<>"]')
12
+
13
+
14
+
15
+ required_sections = ['PROFILE SUMMARY','ACADEMIC PROFILE','TECHNICAL SKILLS','CERTIFICATIONS','PROJECTS','CAREER OBJECTIVE']
16
+
17
+ basic_informations = ["name", "contact_number", "email", "linkedin_urls", "github_urls"]
18
+
19
+ data_science_skills = ['queries', 'beautifulsoup', 'ms excel', 'mathematics', 'selenium',
20
+ 'html', 'analytical skills', 'statsmodels','ai', 'improvement',
21
+ 'analyze', 'metrics', 'forecasting', 'analytics', 'analytical',
22
+ 'mysql', 'postgresql', 'database', 'writing', 'excel','regulations',
23
+ 'algorithms', 'scipy', 'opencv', 'reports', 'eda', 'jupyter',
24
+ 'presentations', 'modeling', 'audit', 'technical skills',
25
+ 'schedule', 'nltk', 'iso', 'xgboost', 'segmentation', 'github',
26
+ 'seaborn', 'keras', 'distribution', 'investigation', 'tableau',
27
+ 'probability', 'analysis', 'r', 'technical', 'programming',
28
+ 'web scraping', 'research', 'pandas', 'statistical analysis',
29
+ 'numpy', 'predictive analysis', 'tensorflow', 'hypothesis',
30
+ 'matplotlib', 'scikit-learn', 'information technology',
31
+ 'machine learning', 'cloud', 'streamlit', 'mining', 'python',
32
+ 'data analytics', 'deep learning', 'testing', 'training',
33
+ 'clustering & classification', 'data analysis', 'engineering',
34
+ 'data visualization', 'quantitative analysis', 'statistics',
35
+ 'flask', 'statistical modeling', 'pytorch', 'data mining',
36
+ 'aws', 'sql']
37
+
38
+ essential_skills = ["Python", "SQL", "MySQL", "Tableau", "NumPy",
39
+ "Statsmodels", "CNN", "ANN",
40
+ "RNN", "Machine Learning", "Deep Learning", "SciKit Learn", "MS Excel",
41
+ "Data Visualization", "Power BI", "Data Analysis"]
42
+
43
+ quality_mapping = {
44
+ 'Resume needs significant improvement': 0.15,
45
+ 'Resume needs improvement': 0.35,
46
+ 'Resume is average': 0.55,
47
+ 'Resume is good': 0.75,
48
+ 'Resume is very good': 0.90,
49
+ 'Resume is excellent': 1,
50
+ 'The resume is bad': 1.1
51
+ }
52
+
53
+ keyword_variations = {
54
+ "Python": ["Python", "Python_Language", "Python Programming"],
55
+ "SQL": ["SQL", "SQL_Language", "Structured Query Language", "Structured_Query_Language"],
56
+ "MySQL": ["MySQL", "MySQL_Database", "My_SQL", "My SQL"],
57
+ "Pandas": ["Pandas", "Pandas_Library", "Pandas Data Analysis Library","Pandas_Data Analysis_Library"],
58
+ "R": [" R ", "R_Programming", "R Language",",R "," R,", ",R,"],
59
+ "Matplotlib": ["Matplotlib", "Matplotlib_Library", "Matplotlib Plotting Library","Matplotlib_Plotting_Library"],
60
+ "Seaborn": ["Seaborn", "Seaborn_Library", "Seaborn Data Visualization Library"],
61
+ "StatsModel": ["StatsModel", "StatsModel_Library", "StatsModel Statistical Library", "Statistical Modeling Library", "Statistics Modeling", "StatModelLib", "StatsMod", "SM Library", "SM"],
62
+ "Tableau": ["Tableau", "Tableau_Software", "Tableau Data Visualization", "Tableau Analytics", "Tableau BI Tool", "Tableau Visualization Software", "Tableau Data Analysis", "Tableau BI","TableauBI"],
63
+ "TensorFlow": ["TensorFlow", "TensorFlow_Library"],
64
+ "NumPy": ["NumPy", "NumPy_Library", "Numerical Computing Library"],
65
+ "PyTorch": ["PyTorch", "PyTorch_Library"],
66
+ "Keras": ["Keras", "Keras_Library"],
67
+ "Plotly": ["Plotly", "Plotly_Library",],
68
+ "RFM": ["RFM", "RFM_Analysis", "Recency Frequency Monetary Analysis"],
69
+ "ANOVA": ["ANOVA", "ANOVA_Test", "Analysis of Variance","Analysis_of_Variance"],
70
+ "BeautifulSoup": ["BeautifulSoup", "BeautifulSoup_Library"],
71
+ "Imputation": ["Imputation", "Data_Imputation","Data Imputation", "Missing Data Imputation"],
72
+ "Scrappy": ["Scrappy", "Scrappy_Library"],
73
+ "Selenium": ["Selenium", "Selenium_Library", "Selenium WebDriver", "Selenium Automation"],
74
+ "TensorBoard": ["TensorBoard", "TensorBoard_Library", "TensorBoard Visualization Tool"],
75
+ "SciPy": ["SciPy", "SciPy_Library", "Scientific Computing Library"],
76
+ "OpenCV": ["OpenCV", "OpenCV_Library", "Computer Vision Library"],
77
+ "NLTK": ["NLTK", "NLTK_Library", "Natural Language Toolkit"],
78
+ "Hadoop": ["Hadoop", "Hadoop_Framework"],
79
+ "Spark": ["Spark", "Spark_Framework", "Apache_Spark"],
80
+ "spacy": ["spacy","Spacy_Library"],
81
+ "AdaBoost": ["AdaBoost","Ada_Boost","Ada Boost", "AdaBoost_Algorithm", "Adaptive Boosting","Adaptive_Boosting"],
82
+ "XGBoost": ["XGBoost","XG_Boost","XG Boost", "XGBoost_Algorithm", "Extreme Gradient Boosting"],
83
+ "CNN": [" CNN ", "CNN,", ",CNN", "Convolutional Neural Network", "ConvNet", "CNN Algorithm","CNN"],
84
+ "ANN": [" ANN ", "ANN,", ",ANN", "Artificial Neural Network", "ANN Algorithm","ANN"],
85
+ "RNN": [" RNN ", "RNN,", ",RNN", "Recurrent Neural Network", "RNN Algorithm","RNN"],
86
+ "KNN": [" kNN ", "kNN,", ",kNN","K-Nearest Neighbours", "K_Nearest_Neighbours", "K-Nearest-Neighbours", "K Nearest Neighbours", "KNN"],
87
+ "LSTM": ["LSTM", "Long Short-Term Memory", "LSTM Network", "LSTM Algorithm"],
88
+ "GAN": [" GAN ", "GAN,", ",GAN", "Generative Adversarial Network", "GAN Algorithm"," GAN "],
89
+ "YOLO": ["YOLO", "You Only Look Once", "YOLO_Algorithm"],
90
+ "Clustering": ["Clustering", "Clustering_Algorithms", "Data_Clustering"],
91
+ "Classification": ["Classification", "Classification_Algorithms", "Data_Classification"],
92
+ "Word2Vec": ["Word2Vec", "Word2Vec_Algorithm", "Word2Vec Word Embeddings","word2vector"],
93
+ "Tf-idf": ["Tf-idf","Tf_idf","Tf idf", "Term Frequency-Inverse Document Frequency", "Tf_idf_Algorithm","Tf-idf_Algorithm"],
94
+ "Tokenization": ["Tokenization", "Text_Tokenization", "Word_Tokenization"],
95
+ "Machine Learning": ["Machine Learning", "Machine_Learning", "Machine Learning Algorithms", "Machine_Learning_Algorithms", "ML"],
96
+ "Deep Learning": ["Deep Learning", "Deep_Learning", "Deep Learning Algorithms", "Deep_Learning_Algorithms", "DL"],
97
+ "SciKit Learn": ["SciKit Learn", "SciKit_Learn", "Sci Kit Learn", "SciKit-Learn","Sci_Kit_Learn", "sklearn","sk_learn"],
98
+ "Hugging Face": ["Hugging Face", "Hugging_Face", "HuggingFace"],
99
+ "MS Excel": ["Excel", "MS Excel","MSExcel", "MS_Excel", "Microsoft_Excel", "Microsoft Excel", "advance_excel","advance_MS_excel","advance_MSexcel", "advance excel", "Advance_Microsoft_excel", "Advance Microsoft excel"],
100
+ "Data Visualization": ["Data Visualization", "Data_Visualization", "Data_Viz", "Visualization"],
101
+ "Power BI": ["Power BI", "Power_BI", "Microsoft_Power_BI", "Microsoft Power BI","PowerBI"],
102
+ "Transfer Learning": ["Transfer Learning", "Transfer_Learning"],
103
+ "Linear Regression": ["Linear Regression", "Linear_Regression"],
104
+ "Logistic Regression": ["Logistic Regression", "Logistic_Regression"],
105
+ "Decision Tree": ["Decision Tree", "Decision_Tree"],
106
+ "Random Forest": ["Random Forest", "Random_Forest"],
107
+ "K-Means Clustering": ["K-Means Clustering", "K_Means_Clustering", "K-Means-Clustering", "K Means Clustering", "K-means", "k_means","K-mean", "k_mean"],
108
+ "T-test": ["T-test", "T_Test", "T Test"],
109
+ "Z-test": ["Z-test", "Z_Test", "Z Test"],
110
+ "Hypothesis Testing": ["Hypothesis Testing", "Hypothesis_Testing"],
111
+ "Chi-square": ["Chi-square", "Chi_Square", "Chi2"],
112
+ "Normal Distribution": ["Normal Distribution", "Normal_Distribution"],
113
+ "Correlation Analysis": ["Correlation Analysis", "Correlation_Analysis"],
114
+ "Feature Scaling": ["Feature Scaling", "Feature_Scaling"],
115
+ "Dimensionality Reduction": ["Dimensionality Reduction", "Dimensionality_Reduction"],
116
+ "Jupyter Notebook": ["Jupyter Notebook", "Jupyter_Notebook"],
117
+ "Google Colab": ["Google Colab", "Google_Colab"],
118
+ "Data Analysis": ["Data Analysis", "Data_Analysis"],
119
+ "Big Data": ["Big Data", "Big_Data"],
120
+ "Support Vector Machines (SVM)": ["Support Vector Machines (SVM)", "Support_Vector_Machines", "SVM", "Support Vector Machines", "Support_Vector_Machines_SVM"],
121
+ "Natural Language Processing": ["Natural Language Processing", "Natural_Language_Processing", "NLP"],
122
+ "Artificial Intelligence": ["Artificial Intelligence", "Artificial_Intelligence"," AI ",",AI "," AI,","AI"],
123
+ "Naive Bayes": ["Naive Bayes", "Naive_Bayes"],
124
+ "Principal Component Analysis (PCA)": ["Principal Component Analysis (PCA)", "Principal_Component_Analysis", "Principal Component Analysis", "PCA"],
125
+ "Descriptive Statistics": ["Descriptive Statistics", "Descriptive_Statistics"],
126
+ "Inferential Statistics": ["Inferential Statistics", "Inferential_Statistics"],
127
+ "Gradient Boosting Machines (GBM)": ["Gradient Boosting Machines (GBM)", "Gradient_Boosting_Machines", "Gradient Boosting Machines", "GBM","Gradient Boosting","Gradient_Boosting"],
128
+ "Association Rule Learning (Apriori)": ["Association Rule Learning (Apriori)", "Association_Rule_Learning", "Association Rule Learning", "Apriori"],
129
+ "Hierarchical Clustering": ["Hierarchical Clustering", "Hierarchical_Clustering"],
130
+ "Image Segmentation": ["Image Segmentation", "Image_Segmentation"],
131
+ "Object Detection": ["Object Detection", "Object_Detection"],
132
+ "Encoder Decoder": ["Encoder - Decoder", "Encoder_Decoder","Encoder Decoder","Encoder Decode",
133
+ "Sequence-to-Sequence Models", "Seq2Seq Models", "Language Encoding", "Language Decoding", "Text Encoding", "Text Decoding",
134
+ "Image Encoding", "Image Decoding", "Audio Encoding", "Audio Decoding", "Video Encoding", "Video Decoding", "Speech Encoding", "Speech Decoding", "Data Compression",
135
+ "Data Encryption", "Data Decryption","Encoder","Decoder"],
136
+ "Word Embedding": ["Word Embedding", "Word_Embedding"],
137
+ "Bag of Words": ["Bag of Words", "Bag_of_Words"],
138
+ "Sentiment Analysis": ["Sentiment Analysis", "Sentiment_Analysis"],
139
+ "Predictive Analysis": ["Predictive Analysis", "Predictive_Analysis"],
140
+ "Statistical Modeling": ["Statistical Modeling", "Statistical_Modeling","Statistical_Analysis","Statistical Analysis"],
141
+ "Data Preprocessing": ["Data Preprocessing", "Data_Preprocessing"],
142
+ "Model Development": ["Model Development", "Model_Development"],
143
+ "Time Series Analysis": ["Time Series Analysis", "Time_Series_Analysis","TimeSeries","TimeSeries_Analysis"],
144
+ "Statistics Fundamentals": ["Statistics Fundamentals", "Statistics_Fundamentals"],
145
+ "Advanced ML": ["Advanced ML", "Advanced_ML", "Advanced Machine Learning", "Advanced_Machine_Learning", "Advanced-ML"],
146
+ "Advanced DL": ["Advanced DL", "Advanced_DL", "Advanced Deep Learning", "Advanced_Deep_Learning", "Advanced-DL"],
147
+ "EDA": ["EDA","Exploratory_Data_Analysis","Exploratory Data Analysis"],
148
+ "Data Mining":["Data Mining","Data_Mining"],
149
+ "Outlier Detection": ["Outlier_Detection","Outlier Detection"],
150
+ "Missing Values Handling": ["Missing Values Handling","Missing_Values_Handling","Missing Values"],
151
+ "Scaling Techniques": ["Scaling Techniques","Feature Scaling","Feature_Scaling","Data Scaling","Data_Scaling","Data Normalization","Data_Normalization","Standardization","Min-Max Scaling","Min-Max_Scaling","Normalization"],
152
+ "R2 and Adjusted R2": ["R2 Score","R2_Score","Adjusted_R2_Score","Adjusted R2 Score","R Squared Score","R_Squared_Score","R2 Accuracy","R2_Accuracy","Adjusted R2 Accuracy","R2 Metric","Adjusted R2 Metric"],
153
+ "Accuracy, Recall, F1 Score": ["Accuracy","Classification_Accuracy","Accuracy_Metrics","Recall","Precision","Recall_Score","F1 Score","F1-Score","F1_Metric","F1_Score","Classification_F1-Score"],
154
+ "MS Office": ["MS_Office","MS Office","Microsoft Office","MS Word","MS_Word","Microsoft_Office","Microsoft_Word","Microsoft Word"],
155
+ "Subquery": ["Subquery","Sub-query","Nested Query","Inner Query"],
156
+ "SQL Join": ["SQL Join","Join in SQL","Join"],
157
+ "Stemming": ["Stemming","Stemming Algorithm","Word Stemming","Stemming Techniques","Stemming in NLP","Text_Stemming","Text Stemming"],
158
+ "Stopwords": ["Stopwords","Stop Words","Common Words","Text Stopwords","Stopwords Removal","Removing Stopwords","Stopwords List","Stopwords in NLP"],
159
+ "docker_variations" : ["Docker Integration","Docker", "Docker Automation", "Advanced Docker","Advanced_Docker", "Docker Tools"],
160
+ "jenkins_variations" : ["Jenkins","Jenkins CI/CD","CI/CD", "Jenkins Automation", "Jenkins Pipeline", "Jenkins Plugins"],
161
+ "prometheus_variations" : ["Prometheus Monitoring", "Prometheus Metrics", "PromQL", "Prometheus Alerting"],
162
+ "cicd_variations" : ["Continuous Integration", "Continuous Deployment", "CI/CD Automation", "CI/CD Tools"],
163
+ "flask_variations" : ["Flask","Flask Framework", "Flask RESTful", "Flask Deployment", "Flask Security"],
164
+ "fastapi_variations" : ["FastAPI","FastAPI Framework", "FastAPI RESTful","FastAPI_RESTful", "FastAPI Deployment", "FastAPI Tools","FastAPI_Tools"],
165
+ "django_variations" : ["Django Framework", "Django Web Development", "Django REST Framework", "Django Deployment","Django"],
166
+ "aws_variations" : ["Amazon Web Services", "AWS Cloud", "AWS Services", "AWS Management","AWS","AWS_Cloud"],
167
+ "statistics_variations" : ["Statistical Analysis", "Descriptive Statistics", "Inferential Statistics", "Probability Theory"],
168
+ "hypothesis_testing_variations" : ["Null Hypothesis", "Alternative Hypothesis", "Significance Level", "Type I Error"],
169
+ "smote_variations" : ["Synthetic Minority Over-sampling Technique", "SMOTE Algorithm", "SMOTE Python", "SMOTE Applications","SMOTE"],
170
+ "mlflow_variations" : ["MLflow Framework", "MLflow Tracking", "MLflow Deployment", "MLflow Integration","MLflow"],
171
+ "packaging_variations" : ["Software Packaging", "Package Management", "Python Packaging", "Packaging Best Practices"],
172
+ "version_control_variations" : ["Git Version Control", "Git Commands", "Git Workflow", "Git Collaboration","Git"],
173
+ "communication skills" : ["communication_skills" , "communication_skill" ,"communication skills"],
174
+ "problem-solving": ["problem-solving","problem_solving", "problem-solving"],
175
+ "decision making" : ["decision making" , "decision-making","decision_making"]
176
+
177
+ }
178
+
179
+ Extract_sections = ["CAREER OBJECTIVE", "PROFILE SUMMARY"]
180
+
181
+ section_headers = [
182
+ "CAREER OBJECTIVE", "PROFILE SUMMARY", "WORK EXPERIENCE", "EDUCATION","ADDITIONAL INFORMATION AND HOBBIES",
183
+ "ACADEMIC PROFILE", "PROJECTS", "CERTIFICATIONS","SKILLS",
184
+ "PERSONAL SKILLS", "PERSONAL INFORMATION", "REFERENCES",
185
+ "EXTRACURRICULAR ACTIVITIES", "TECHNICAL SKILLS", "KEY SKILLS",
186
+ "ADDITIONAL INFORMATION", "CERTIFICATIONS & ACADEMIC ENDEAVOURS",
187
+ "AWARDS & ACCOLADES", "SOFTWARE SKILLS", "AWARDS"
188
+ ]
189
+
190
+ common_projects = ["Titanic","Iris","MNIST", "COVID-19", "Bank Churn",
191
+ "Spam","Handwritten Digit","Heart Disease","House Price",
192
+ "Diabetes","Twitter", "Churn",
193
+ "Wine Quality", "Loan","Titanic Survival Prediction",
194
+ "Iris Flower Classification",
195
+ "House Price Prediction",
196
+ "MNIST Handwritten Digit Recognition",
197
+ "Customer Churn Prediction",
198
+ "Sentiment Analysis of Movie Reviews",
199
+ "Spam Email Detection",
200
+ "Fake News Detection",
201
+ "Image Classification with CNNs",
202
+ "Stock Price Prediction"]
203
+
204
+ suggested_projects = ["Predicting Patient Readmissions in Hospitals",
205
+ "Optimizing Ad Spend with Machine Learning Models","Developing a Fake News Detection System",
206
+ "Developing an AI Chatbot for Customer Service Automation","Personalized Health Recommendations Using Wearable Data"]
207
+
208
+ # Specify rule IDs and error keywords to ignore
209
+ ignore_rule_ids = ['WHITESPACE_RULE']
210
+ ignore_error_keywords = ['repeated a whitespace']
211
+
212
+ # Blogs & Articles
213
+ blog_articles = ["https://www.dataquest.io/blog/how-data-science-resume-cv/",
214
+ "https://medium.com/data-science-at-microsoft/writing-a-resume-for-a-data-science-role-345b98bdf80b",
215
+ "https://medium.com/@alicechen.ai/resume-201-how-to-write-an-effective-data-science-resume-441cbe6c0932"
216
+ ]
217
+
218
+ # Links
219
+ youtube_links = ["https://youtu.be/Tt08KmFfIYQ?si=EdebdWUfbttysrfL",
220
+ "https://youtu.be/R3abknwWX7k?si=m4EyviXgKDoPgIGr",
221
+ "https://youtu.be/1-z9ptlBar4?si=lA7WgU4j4MFGjBZV",
222
+ "https://youtu.be/pjqi_M3SPwY?si=5aRizcfpreKR9xUr",
223
+ "https://youtu.be/ROfceyeD7f4?si=OTbrL7BUKSW1u2mt"]
src/logging_config.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/logging_config.py
2
+ import logging
3
+ from logging.handlers import TimedRotatingFileHandler
4
+ import os
5
+ from datetime import datetime
6
+
7
+ def setup_logging():
8
+ # Create the logs directory if it doesn't exist
9
+ log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
10
+ if not os.path.exists(log_dir):
11
+ os.makedirs(log_dir)
12
+
13
+ # Set the logging level to DEBUG
14
+ logger = logging.getLogger()
15
+ logger.setLevel(logging.DEBUG)
16
+
17
+ # Create a console handler to output logs to the console
18
+ console_handler = logging.StreamHandler()
19
+ console_handler.setLevel(logging.DEBUG)
20
+
21
+ # Create a timed rotating file handler to output logs to a file
22
+ today_date = datetime.now().strftime('%Y-%m-%d')
23
+ log_file = os.path.join(log_dir, f'{today_date}.log')
24
+ file_handler = TimedRotatingFileHandler(log_file, when='midnight', interval=1)
25
+ file_handler.setLevel(logging.DEBUG)
26
+ file_handler.suffix = '%Y-%m-%d' # Date format for log file names
27
+
28
+ # Create a formatter and set it for both handlers
29
+ formatter = logging.Formatter(
30
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
31
+ console_handler.setFormatter(formatter)
32
+ file_handler.setFormatter(formatter)
33
+
34
+ # Add both handlers to the logger
35
+ logger.addHandler(console_handler)
36
+ logger.addHandler(file_handler)
37
+
38
+ # Avoid duplicate logs by removing the default handler if present
39
+ if logger.hasHandlers():
40
+ logger.handlers.clear()
41
+ logger.addHandler(console_handler)
42
+ logger.addHandler(file_handler)
src/main.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, jsonify, request, flash, redirect, url_for, render_template
3
+ from submitter import ResumeSubmitter
4
+ from reviewer import ResumeReviewer
5
+ from resume_parser import ResumeParser
6
+ from logging_config import setup_logging
7
+ import json
8
+
9
+ app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), '..', 'templates'))
10
+ app.secret_key = 'supersecretkey'
11
+ setup_logging()
12
+
13
+ @app.route('/v1/resumes/', methods=['POST', 'GET'])
14
+ def submit_resume():
15
+ if request.method == 'POST':
16
+ result = ResumeSubmitter().upload_file()
17
+ if os.path.exists(result):
18
+ resume_path = result # Get the path of the uploaded resume
19
+ try:
20
+ # Redirect to the /v1/reviews/ endpoint with the resume path as a parameter
21
+ return redirect(url_for('get_reviews', path=resume_path))
22
+ except Exception as e:
23
+ app.logger.error("Failed to redirect to /v1/reviews/: %s", str(e))
24
+ return jsonify(message="failed to redirect to reviews page"), 500
25
+ else:
26
+ return jsonify(message=f"failed to submit resume, {result}"), 400
27
+ else:
28
+ return ResumeSubmitter().upload_form()
29
+
30
+ @app.route("/v1/reviews/<path:path>", methods=['POST', 'GET'])
31
+ def get_reviews(path):
32
+ app.logger.debug("Inside get_reviews")
33
+ resume_parser = ResumeParser()
34
+ resume_reviewer = ResumeReviewer()
35
+ parsed_resume_response = resume_parser.parse_text(path)
36
+
37
+ # Check if the response data is in JSON format
38
+ try:
39
+ # Assuming parsed_resume_response.data contains the JSON string
40
+ parsed_resume_dict = json.loads(parsed_resume_response.data)
41
+ except json.JSONDecodeError:
42
+ app.logger.error("Failed to decode JSON from the response")
43
+ return "Invalid JSON response from parser", 500
44
+
45
+ # Save the dictionary as JSON file (optional, if you want to save it to a file)
46
+ with open('parsed_resume.json', 'w') as json_file:
47
+ json.dump(parsed_resume_dict, json_file)
48
+
49
+ # Pass the dictionary to the template
50
+ return render_template("review_output.html", parsed_resume=parsed_resume_dict)
51
+
52
+ @app.route("/v1/users/<int:id>", methods=['GET'])
53
+ def get_user(id):
54
+ return jsonify(message="user retrieved successfully! for given id {}".format(id))
55
+
56
+ @app.route('/', methods=['GET'])
57
+ def greet():
58
+ return render_template('home_page.html')
59
+
60
+ if __name__ == '__main__':
61
+ app.run()
src/resume_parser.py ADDED
@@ -0,0 +1,1208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #total score = 67
2
+ # python file to parse different section from resume
3
+ from pdfminer.high_level import extract_pages, extract_text
4
+ from pdfminer.layout import LTTextContainer, LTChar, LTTextLineHorizontal
5
+ from collections import defaultdict
6
+ from flask import jsonify
7
+ import re, fitz, requests, logging, datetime
8
+ import src.config as config
9
+ from .config import data_science_skills, keyword_variations, essential_skills, quality_mapping, Extract_sections, suggested_projects, ignore_rule_ids
10
+ from .config import required_sections, linkedin_domain, github_domain, basic_informations, section_headers, common_projects, ignore_error_keywords,blog_articles,youtube_links
11
+ from .config import kaggle_domain,hackerrank_domain,leetcode_domain,medium_domain
12
+ from spacy.matcher import Matcher
13
+ import language_tool_python
14
+ from collections import defaultdict
15
+ import random
16
+ tool = language_tool_python.LanguageTool('en-US')
17
+
18
+
19
+
20
+ class ResumeParser:
21
+
22
+ def extract_contact_number_from_resume(self, text):
23
+ contact_number = None
24
+ suggestion = ""
25
+
26
+ # Use regex pattern to find a potential contact number
27
+ pattern = r"\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b"
28
+ match = re.search(pattern, text)
29
+ if match:
30
+ contact_number = match.group()
31
+ # Check if the contact number is of the correct length
32
+ digits_only = re.sub(r'\D', '', contact_number)
33
+ if len(digits_only) == 10:
34
+ suggestion = ""
35
+ elif len(digits_only) > 10 and digits_only.startswith('91') and len(digits_only[2:]) == 10:
36
+ suggestion = ""
37
+ else:
38
+ suggestion = "Contact number should have exactly 10 digits."
39
+
40
+ return contact_number, suggestion
41
+
42
+
43
+
44
+ def extract_hyperlinks(self, pdf_path):
45
+ doc = fitz.open(pdf_path)
46
+ links = []
47
+
48
+ for page_num in range(len(doc)):
49
+ page = doc.load_page(page_num)
50
+ link_list = page.get_links()
51
+ for link in link_list:
52
+ uri = link.get('uri', None)
53
+ if uri:
54
+ links.append(uri)
55
+
56
+ return links
57
+
58
+ def extract_text_from_pdf(self, pdf_path):
59
+ return extract_text(pdf_path)
60
+
61
+ def extract_email_from_text(self, text):
62
+ pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"
63
+ match = re.search(pattern, text)
64
+ if match:
65
+ return match.group()
66
+ return None
67
+
68
+ def extract_email_from_resume(self, pdf_path):
69
+ text = self.extract_text_from_pdf(pdf_path)
70
+ email = self.extract_email_from_text(text)
71
+ suggestion = ""
72
+
73
+ # If no email found in text, check hyperlinks
74
+ if not email:
75
+ links = self.extract_hyperlinks(pdf_path)
76
+ for link in links:
77
+ if link.startswith('mailto:'):
78
+ email_candidate = link.split('mailto:')[1]
79
+ if self.is_valid_email(email_candidate):
80
+ email = email_candidate
81
+ break
82
+
83
+ # Additional validation for email found in text or links
84
+ if email and not self.is_valid_email(email):
85
+ suggestion += "Your email address doesn't seem to be valid. Please check and correct."
86
+
87
+ return email, suggestion
88
+
89
+
90
+ def is_valid_email(self, email):
91
+ # Length check
92
+ if len(email) > 254:
93
+ return False
94
+
95
+ # Consecutive special characters check
96
+ if re.search(r"[._%+-]{2,}", email):
97
+ return False
98
+
99
+ # Domain part validation
100
+ domain_part = email.split('@')[1]
101
+ if not re.match(r"[A-Za-z0-9.-]+\.[A-Za-z]{2,}", domain_part):
102
+ return False
103
+
104
+ # Standard email format check
105
+ pattern = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
106
+ return re.match(pattern, email) is not None
107
+
108
+
109
+ def extract_sections_from_resume(self, text):
110
+ missing_sections = []
111
+ sections_not_capitalized = []
112
+
113
+ for section in required_sections:
114
+ pattern = r"\b{}\b".format(re.escape(section))
115
+
116
+ match_obj = re.search(pattern, text, re.IGNORECASE)
117
+ if not match_obj:
118
+ missing_sections.append(section)
119
+ else:
120
+ if match_obj.group() not in map(str.upper, required_sections):
121
+ sections_not_capitalized.append(section)
122
+
123
+ return missing_sections, sections_not_capitalized
124
+
125
+ def extract_skills_from_resume(self, text):
126
+ if not isinstance(text, str):
127
+ raise ValueError(f"Expected 'text' to be a string, but got {type(text)}")
128
+
129
+ skills = []
130
+ for skill in essential_skills:
131
+ pattern = r"\b{}\b".format(re.escape(skill))
132
+ match = re.search(pattern, text, re.IGNORECASE)
133
+ if match:
134
+ skills.append(skill)
135
+ return skills
136
+
137
+ def extract_keyword_variations_from_resume(self, text):
138
+ found_keywords = []
139
+ for keyword, variations in keyword_variations.items():
140
+ for variation in variations:
141
+ if variation.lower() in text.lower():
142
+ found_keywords.append(variation)
143
+ break
144
+
145
+ return found_keywords
146
+
147
+ def extract_keyword_variations_from_formatted_text(self, formatted_text):
148
+ found_keyword_section = []
149
+ for keyword, variations in keyword_variations.items():
150
+ for variation in variations:
151
+ if variation.lower() in formatted_text.lower():
152
+ found_keyword_section.append(variation)
153
+ break
154
+
155
+ return found_keyword_section
156
+
157
+ def extract_linkedIn_urls_from_pdf(self, pdf_path):
158
+ linkedin_urls = None
159
+ pdf_document = fitz.open(pdf_path)
160
+ for page_num in range(len(pdf_document)):
161
+ page = pdf_document.load_page(page_num)
162
+ links = page.get_links()
163
+ for link in links:
164
+ url = link.get('uri', '')
165
+ if re.search(linkedin_domain, url):
166
+ linkedin_urls = url
167
+ pdf_document.close()
168
+ return linkedin_urls
169
+
170
+ def extract_github_urls_from_pdf(self, pdf_path):
171
+ github_urls = None
172
+ pdf_document = fitz.open(pdf_path)
173
+ for page_num in range(len(pdf_document)):
174
+ page = pdf_document.load_page(page_num)
175
+ links = page.get_links()
176
+ for link in links:
177
+ url = link.get('uri', '')
178
+ if re.search(github_domain, url):
179
+ path = re.sub(github_domain, '', url)
180
+ parts = path.split('/')
181
+ if len(parts) == 1:
182
+ github_urls = url
183
+ pdf_document.close()
184
+ return github_urls
185
+
186
+
187
+ def extract_extra_urls_pdf(self,pdf_path, domains):
188
+ extracted_urls = defaultdict(set)
189
+ try:
190
+ # Open the PDF document
191
+ pdf_document = fitz.open(pdf_path)
192
+
193
+ # Iterate through all pages in the PDF
194
+ for page_num in range(len(pdf_document)):
195
+ page = pdf_document.load_page(page_num)
196
+ links = page.get_links()
197
+
198
+ for link in links:
199
+ url = link.get('uri', '')
200
+ if url: # Ensure there's a URL
201
+ for domain in domains:
202
+ if re.search(domain, url, re.IGNORECASE):
203
+ extracted_urls[domain].add(url) # Add URL to the domain's set
204
+ except Exception as e:
205
+ print(f"Error processing PDF: {e}")
206
+ finally:
207
+ pdf_document.close()
208
+
209
+ return {domain: list(urls) for domain, urls in extracted_urls.items()}
210
+
211
+ def is_valid_url(self , github_urls ):
212
+ suggest = ""
213
+ for _ in [github_urls]:
214
+ if not github_urls:
215
+ break
216
+
217
+ try:
218
+ response = requests.head(github_urls)
219
+ if response.status_code != 200:
220
+ suggest = "GitHub URL is not valid, please check and correct. "
221
+ except requests.RequestException:
222
+ suggest = "GitHub URL is not valid, please check and correct. "
223
+
224
+ return suggest
225
+ return suggest
226
+
227
+
228
+ def is_valid_name(self, name):
229
+ if any(char.isdigit() for char in name):
230
+ return False
231
+ if len(name.split()) > 3:
232
+ return False
233
+ common_non_names = {"Email", "Github", "LinkedIn", "Portfolio", "Data Analyst"}
234
+ if name in common_non_names:
235
+ return False
236
+ return True
237
+
238
+ def extract_name(self, resume_text):
239
+
240
+ lines = resume_text.split('\n')
241
+
242
+ # Use regex to find lines that likely contain names
243
+ name_lines = [line for line in lines if re.match(r'^[A-Za-z]*\s[A-Za-z]*', line.strip())]
244
+
245
+ names = []
246
+ for i in range(len(name_lines)):
247
+ if self.is_valid_name(name_lines[i].strip()):
248
+ names.append(name_lines[i].strip())
249
+
250
+ if len(names) >= 1:
251
+ name = names[0]
252
+ suggestion = ""
253
+ # Check if the name parts contain only alphabetic characters
254
+ name_parts = name.split()
255
+ if any(part[0].islower() for part in name_parts):
256
+ suggestion += " name should start with a capital letter. "
257
+ return name, suggestion
258
+
259
+ return None, "No valid name found"
260
+
261
+
262
+ def check_missing_sections(self, resume_data):
263
+ missing_information = []
264
+ for section in basic_informations:
265
+ if not resume_data.get(section):
266
+ missing_information.append(section)
267
+ return missing_information
268
+
269
+ def segregate_sections(self, text):
270
+ header_pattern = re.compile(rf'^\s*({"|".join(re.escape(header) for header in section_headers)}):?\s*$', re.IGNORECASE)
271
+ sections_text = {}
272
+ current_section = None
273
+ lines = text.splitlines()
274
+ for line in lines:
275
+ clean_line = line.strip()
276
+ match = header_pattern.match(clean_line)
277
+ if match:
278
+ current_section = match.group(1).upper()
279
+ sections_text[current_section] = []
280
+ elif current_section:
281
+ sections_text[current_section].append(line.strip())
282
+
283
+ return sections_text
284
+
285
+ def extract_and_format_sections(self, sections_text, Extract_sections):
286
+ formatted_text = ""
287
+ for section in Extract_sections:
288
+ if section in sections_text:
289
+ section_content = " ".join(sections_text[section]).replace('\n', ' ')
290
+ formatted_text += f"{section}:\n{section_content}\n\n"
291
+ return formatted_text
292
+
293
+ def replace_keywords_with_placeholders(self, formatted_text, found_keyword_section):
294
+ placeholder_text = formatted_text
295
+ keyword_placeholders = {}
296
+
297
+ # Use a set to avoid duplicates and keep track of keyword placeholders
298
+ used_keywords = set()
299
+ for i, keyword in enumerate(found_keyword_section):
300
+ if keyword not in used_keywords:
301
+ used_keywords.add(keyword)
302
+ placeholder = f"{{KEYWORD_{i}}}"
303
+ keyword_placeholders[placeholder] = keyword
304
+ # Using word boundary to match whole words
305
+ placeholder_text = re.sub(r'\b' + re.escape(keyword) + r'\b', placeholder, placeholder_text, flags=re.IGNORECASE)
306
+
307
+ return placeholder_text, keyword_placeholders
308
+
309
+ def replace_placeholders_with_keywords(self, grammar_issues, keyword_placeholders):
310
+ updated_issues = []
311
+ for issue in grammar_issues:
312
+ context = issue['context']
313
+ for placeholder, keyword in keyword_placeholders.items():
314
+ context = context.replace(placeholder, keyword)
315
+ # Update the context in the issue dictionary
316
+ issue['context'] = context
317
+ updated_issues.append(issue)
318
+ return updated_issues
319
+
320
+ def grammar_check(self, placeholder_text):
321
+ matches = tool.check(placeholder_text)
322
+ grammar_issues = []
323
+ for match in matches:
324
+ issue = {
325
+ "context": match.context,
326
+ "error": match.message,
327
+ "rule_id": match.ruleId,
328
+ "suggested_correction": match.replacements
329
+ }
330
+ grammar_issues.append(issue)
331
+ return grammar_issues
332
+
333
+ def filter_grammar_issues(self, grammar_issues, ignore_rule_ids=None, ignore_error_keywords=None):
334
+ if ignore_rule_ids is None:
335
+ ignore_rule_ids = []
336
+ if ignore_error_keywords is None:
337
+ ignore_error_keywords = []
338
+
339
+ filtered_issues = []
340
+ for issue in grammar_issues:
341
+ if issue['rule_id'] not in ignore_rule_ids and not any(keyword in issue['error'] for keyword in ignore_error_keywords):
342
+ filtered_issues.append(issue)
343
+
344
+ return filtered_issues
345
+
346
+ def process_resume(self, text, found_keyword_section, Extract_sections):
347
+ sections_text = self.segregate_sections(text)
348
+ formatted_text = self.extract_and_format_sections(sections_text, Extract_sections)
349
+ found_keyword_section = self.extract_keyword_variations_from_formatted_text(formatted_text)
350
+ placeholder_text, keyword_placeholders = self.replace_keywords_with_placeholders(formatted_text, found_keyword_section)
351
+ grammar_issues = self.grammar_check(placeholder_text)
352
+ grammar_issues_text = self.replace_placeholders_with_keywords(grammar_issues, keyword_placeholders)
353
+ filtered_grammar_issues = self.filter_grammar_issues(grammar_issues, ignore_rule_ids, ignore_error_keywords)
354
+ return {
355
+ "grammar_issues": filtered_grammar_issues,
356
+ "spelling_errors": [issue for issue in filtered_grammar_issues if "SPELLING" in issue['rule_id']]
357
+ }
358
+
359
+ def grammar_issue_check(self, text, found_keyword_section, Extract_sections):
360
+ issues = {}
361
+ text1 = " ".join(text.split("\n"))
362
+ for section in Extract_sections:
363
+ grammar_issues = self.process_resume(text, found_keyword_section, [section])
364
+ if not grammar_issues:
365
+ grammar_issues = "no error found"
366
+ issues[section] = grammar_issues
367
+ return issues
368
+
369
+ def normalize_font_name(self,font_name):
370
+ if '-' in font_name:
371
+ font_name = font_name.split('-')[0]
372
+ if '+' in font_name:
373
+ font_name = font_name.split('+')[1]
374
+ return font_name
375
+
376
+
377
+ def extract_text_properties(self, pdf_path, predefined_terms):
378
+ text_properties = []
379
+ current_phrase = ""
380
+ current_font_size = None
381
+ current_font_name = None
382
+ current_page_num = None
383
+
384
+ special_characters = set("●β–ͺβ€’!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
385
+
386
+ def add_current_phrase():
387
+ nonlocal current_phrase
388
+ if current_phrase.strip():
389
+ flag = any(current_phrase in term for term in predefined_terms)
390
+ if not flag:
391
+ text_properties.append({
392
+ "text": current_phrase,
393
+ "font_size": current_font_size,
394
+ "font_name": current_font_name,
395
+ "page_num": current_page_num
396
+ })
397
+ current_phrase = ""
398
+
399
+ for page_layout in extract_pages(pdf_path):
400
+ for element in page_layout:
401
+ if isinstance(element, LTTextContainer):
402
+ for text_line in element:
403
+ if isinstance(text_line, LTTextLineHorizontal):
404
+ for character in text_line:
405
+ if isinstance(character, LTChar):
406
+ text = character.get_text()
407
+ font_size = round(character.size, 2)
408
+ font_name = self.normalize_font_name(character.fontname)
409
+ page_num = page_layout.pageid
410
+
411
+ if text.isspace() or text in special_characters:
412
+ add_current_phrase()
413
+ continue
414
+
415
+ if (font_size != current_font_size or font_name != current_font_name or
416
+ page_num != current_page_num):
417
+ add_current_phrase()
418
+ current_font_size = font_size
419
+ current_font_name = font_name
420
+ current_page_num = page_num
421
+
422
+ current_phrase += text
423
+
424
+ add_current_phrase()
425
+
426
+ return text_properties
427
+
428
+ def group_similar_fonts(self,text_properties, tolerance=0.5):
429
+ grouped_properties = defaultdict(list)
430
+
431
+ for prop in text_properties:
432
+ rounded_size = round(prop["font_size"] / tolerance) * tolerance
433
+ key = (prop["font_name"], rounded_size)
434
+ grouped_properties[key].append(prop)
435
+
436
+ return grouped_properties
437
+
438
+
439
+
440
+
441
+ def identify_different_fonts_and_sizes(self, grouped_properties):
442
+ most_common_group = max(grouped_properties.values(), key=len)
443
+ most_common_key = None
444
+ for key, group in grouped_properties.items():
445
+ if group == most_common_group:
446
+ most_common_key = key
447
+ break
448
+
449
+ different_texts = []
450
+
451
+ for key, group in grouped_properties.items():
452
+ if group != most_common_group:
453
+ for prop in group:
454
+ reason = []
455
+ if key[1] != most_common_key[1]:
456
+ reason.append(f"size not {most_common_key[1]}")
457
+ if key[0] != most_common_key[0]:
458
+ reason.append(f"font not {most_common_key[0]}")
459
+ different_texts.append({
460
+ "page_num": prop['page_num'],
461
+ "text": prop['text'],
462
+ "found_size": prop['font_size'],
463
+ "found_font_name": prop['font_name'],
464
+ "reason": ", ".join(reason)
465
+ })
466
+
467
+ return different_texts
468
+
469
+ def parse_dates(self, sections_text, section_name):
470
+ # Check if the section is in the text
471
+ suggest = ""
472
+
473
+ # Define the date patterns to match various date formats
474
+ date_pattern = (
475
+ r'\b\d{1,2}/\d{4}\b|' # MM/YYYY
476
+ r'\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{4}\b|' # Month YYYY
477
+ r'\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2},?\s*\d{4}\b|' # Month DD, YYYY
478
+ r'\b\d{4}\b|' # YYYY
479
+ r'\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)[a-z]*/?\d{4}\b|' # Month/YYYY
480
+ r'\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)[a-z]*\d{4}\s*-\s*(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)[a-z]*\d{4}\b' # Month/YYYY - Month/YYYY
481
+ )
482
+
483
+ all_dates = []
484
+
485
+ # Iterate over the entries in the section_name
486
+ for entry in sections_text[section_name]:
487
+ entry = entry.lower()
488
+ matches = re.findall(date_pattern, entry)
489
+ if matches and len(matches)>1:
490
+ if len(matches) == 2:
491
+ all_dates.append(f"{matches[0]} {matches[1]}")
492
+ else:
493
+ all_dates.extend(matches)
494
+
495
+ return all_dates
496
+
497
+
498
+ def convert_to_date(self, date_str):
499
+ # Mapping of month names and abbreviations to their numeric equivalents
500
+ month_map = {
501
+ 'jan': 1, 'january': 1, 'feb': 2, 'february': 2,
502
+ 'mar': 3, 'march': 3, 'apr': 4, 'april': 4,
503
+ 'may': 5, 'jun': 6, 'june': 6, 'jul': 7,
504
+ 'july': 7, 'aug': 8, 'august': 8, 'sep': 9,
505
+ 'september': 9, 'oct': 10, 'october': 10,
506
+ 'nov': 11, 'november': 11, 'dec': 12, 'december': 12,
507
+ '01': 1, '02': 2, '03': 3, '04': 4,
508
+ '05': 5, '06': 6, '07': 7, '08': 8,
509
+ '09': 9, '10': 10, '11': 11, '12': 12
510
+ }
511
+
512
+ # Regex patterns to match different date formats
513
+ pattern_mm_yyyy = re.compile(r'(\d{1,2})/(\d{4})')
514
+ pattern_mm_yyyy_space = re.compile(r'(\d{1,2})\s(\d{4})')
515
+ pattern_month_yyyy = re.compile(r'([a-zA-Z]+)\s?(\d{4})')
516
+ pattern_yyyy = re.compile(r'(\d{4})')
517
+
518
+ def extract_date(date_str):
519
+ match_mm_yyyy = pattern_mm_yyyy.match(date_str)
520
+ match_mm_yyyy_space = pattern_mm_yyyy_space.match(date_str)
521
+ match_month_yyyy = pattern_month_yyyy.match(date_str)
522
+ match_yyyy = pattern_yyyy.match(date_str)
523
+
524
+ if match_mm_yyyy:
525
+ month = int(match_mm_yyyy.group(1))
526
+ year = int(match_mm_yyyy.group(2))
527
+ elif match_mm_yyyy_space:
528
+ month = int(match_mm_yyyy_space.group(1))
529
+ year = int(match_mm_yyyy_space.group(2))
530
+ elif match_month_yyyy:
531
+ month = month_map.get(match_month_yyyy.group(1).lower())
532
+ year = int(match_month_yyyy.group(2))
533
+ elif match_yyyy:
534
+ month = 1
535
+ year = int(match_yyyy.group(1))
536
+ else:
537
+ return []
538
+
539
+ return datetime.date(year, month, 1)
540
+
541
+ date_parts = re.findall(r'(\d{4}\s[a-zA-Z]+\s?|\d{4}[a-zA-Z]+|\d{4}\/\d{2}|\d{4}\s\d{2}|[a-zA-Z]+\s?\d{4}|\d{4}\s[a-zA-Z]+)', date_str)
542
+ if len(date_parts) == 1:
543
+ # Standalone year or single date
544
+ start_date = extract_date(date_parts[0])
545
+ end_date = datetime.date(start_date.year, start_date.month, start_date.day)
546
+ elif len(date_parts) == 2:
547
+ # Date range
548
+ start_date = extract_date(date_parts[0])
549
+ end_date = extract_date(date_parts[1])
550
+ else:
551
+ return []
552
+
553
+ return start_date, end_date
554
+
555
+
556
+ def date_time(self, date_parts):
557
+ converted_dates = []
558
+ for date_part in date_parts:
559
+ start_date, end_date = self.convert_to_date(date_part)
560
+ converted_dates.append((start_date, end_date))
561
+ return converted_dates
562
+
563
+
564
+ def check_chronological_order(self, converted_dates, section_name ):
565
+ suggestion = ""
566
+ sorted_dates = sorted(converted_dates, key=lambda x: (x[1], x[0]), reverse=True)
567
+ if converted_dates == sorted_dates:
568
+ suggestion = f"{section_name} section is in chronological order."
569
+ else:
570
+ suggestion = f"{section_name} section is not in chronological order."
571
+
572
+ return suggestion
573
+
574
+ def check_common_projects(self, projects_text):
575
+ found_projects = []
576
+ for project in common_projects:
577
+ if project.lower() in projects_text.lower():
578
+ found_projects.append(project)
579
+ return found_projects
580
+
581
+ def recommend_resources():
582
+ # Randomly pick 2 blog articles and 2 YouTube links
583
+ recommended_blogs = random.sample(blog_articles, 2)
584
+ recommended_youtube = random.sample(youtube_links, 2)
585
+
586
+ # Return the recommendations
587
+ return {
588
+ "Recommended Blogs": recommended_blogs,
589
+ "Recommended YouTube Links": recommended_youtube
590
+ }
591
+
592
+ def check_imarticus_certifications(self, certifications_text):
593
+ # Check if "imarticus" is present in the certifications text
594
+ if "imarticus" in certifications_text.lower():
595
+ return {
596
+ "found": True,
597
+ "message": "Imarticus certification found. Please upload it in the academic section."
598
+ }
599
+ return {
600
+ "found": False,
601
+ "message": "No Imarticus certification found in the provided text."
602
+ }
603
+
604
+
605
+ def chronological_order_check(self, sections_text, section_name):
606
+ order_suggestion = ""
607
+ suggestion = ""
608
+ section_name = section_name.upper()
609
+ if section_name in sections_text:
610
+ date = self.parse_dates(sections_text, section_name)
611
+ if date:
612
+ converted_dates = self.date_time(date)
613
+ order_suggestion = self.check_chronological_order(converted_dates, section_name)
614
+ else:
615
+ suggestion = f"No valid dates found in {section_name} section. "
616
+ else:
617
+ suggestion = f"{section_name} is not in section header. "
618
+
619
+ return order_suggestion, suggestion
620
+
621
+
622
+
623
+ # Function to check for spelling mistakes
624
+ def check_spelling(self, headers, section_headers):
625
+ suggestions = []
626
+ for header in headers:
627
+ if header.upper() not in map(str.upper, section_headers):
628
+ suggestions = header
629
+ return suggestions
630
+
631
+ def is_present_name(name):
632
+ """
633
+ Checks if a given name has at least 2 words.
634
+
635
+ Args:
636
+ name: The name string to check.
637
+
638
+ Returns:
639
+ True if it has at least 2 words, false otherwise.
640
+ """
641
+ parts = name.split()
642
+ return len(parts) >= 2
643
+
644
+ def is_sentence_case(name):
645
+
646
+ parts = name.split() # Split into individual words
647
+ for part in parts:
648
+ if not part: # handles empty strings in name
649
+ continue
650
+ if not part[0].isupper() or not part[1:].islower():
651
+ return False # Check if first letter is uppercase and rest are lowercase
652
+ return True
653
+
654
+ def is_present_name(self,name):
655
+ parts = name.split()
656
+ return len(parts) >= 2
657
+
658
+ def is_sentence_case(self,name):
659
+ parts = name.split()
660
+ for part in parts:
661
+ if not part:
662
+ continue
663
+ if not part[0].isupper() or not part[1:].islower():
664
+ return False
665
+ return True
666
+
667
+ def extract_project_links(self,sections_text):
668
+ project_links = {}
669
+
670
+ if "PROJECTS" in sections_text:
671
+ project_list = sections_text.get("PROJECTS", [])
672
+ url_pattern = r"https?://[^\s]+"
673
+ for project in project_list:
674
+ links = re.findall(url_pattern,project)
675
+ if links:
676
+ project_links[project] = links
677
+ return project_links
678
+
679
+ def count_sentences(self,text):
680
+ sentence_endings = r"(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|!)\s"
681
+ sentences = re.split(sentence_endings, text)
682
+ sentences = [s.strip() for s in sentences if s.strip()]
683
+ return len(sentences)
684
+
685
+ def calculate_summary_score(self, summary):
686
+ score = 0 # Initialize score
687
+ if not summary:
688
+ return score
689
+
690
+ num_sentences = self.count_sentences(summary)
691
+ if num_sentences <= 2:
692
+ return 2
693
+ elif num_sentences > 2 and num_sentences <= 4:
694
+ return 3
695
+ elif num_sentences > 4:
696
+ return 5
697
+ else:
698
+ return 0
699
+
700
+ def calculate_extra_urls_bonus(self,pdf_path):
701
+ domains = [
702
+ r"hackerrank\.com", # Hackerrank
703
+ r"leetcode\.com", # LeetCode
704
+ r"medium\.com" # Medium
705
+ ]
706
+ extra_urls = self.extract_extra_urls_pdf(pdf_path, domains)
707
+ has_extra_urls = any(urls for urls in extra_urls.values())
708
+ return 5 if has_extra_urls else 0
709
+
710
+ def calculate_relevant_experience_score(self, experience_text):
711
+ """
712
+ Assigns a score based on the presence of relevant experience keywords.
713
+
714
+ Args:
715
+ experience_text (str): The extracted work experience section text.
716
+
717
+ Returns:
718
+ int: A score of 5 if relevant keywords are found, otherwise 0.
719
+ """
720
+ if not experience_text:
721
+ return 0 # βœ… No experience section β†’ Score 0
722
+
723
+ if isinstance(experience_text, list):
724
+ experience_text = " ".join(experience_text) # βœ… Convert list to a single string
725
+
726
+ experience_text = experience_text.strip().lower() # βœ… Ensure it's a string and lowercase
727
+
728
+ # βœ… Check if any keyword from 'data_science_skills' or 'essential_skills' exists
729
+ for skill in config.data_science_skills + config.essential_skills:
730
+ if skill.lower() in experience_text:
731
+ return 5 # βœ… Found relevant experience β†’ Full score
732
+
733
+ return 0
734
+
735
+ def calculate_ds_skills_score(self, skills_present):
736
+ if not skills_present: # No skills found at all
737
+ return 0
738
+
739
+ # Use skills from config instead of hardcoded list
740
+ ds_skills_list_lower = [skill.lower() for skill in config.data_science_skills]
741
+ skills_present_lower = [skill.lower() for skill in skills_present]
742
+
743
+ matching_count = sum(1 for skill in skills_present_lower
744
+ if skill in ds_skills_list_lower)
745
+
746
+ if matching_count == 0: # Skills found but none match DS list
747
+ return 2
748
+ elif 1 <= matching_count <= 5:
749
+ return 3
750
+ elif matching_count > 5:
751
+ return 5
752
+ return 0
753
+
754
+ def calculate_project_link_score(self, projects_with_links):
755
+ """
756
+ Assigns a score based on whether project links are present.
757
+
758
+ Args:
759
+ projects_with_links (int): The number of projects with links.
760
+
761
+ Returns:
762
+ int: 2 if project links are found, otherwise 0.
763
+ """
764
+ return 2 if projects_with_links > 0 else 0
765
+
766
+
767
+ def imarticus_review_score(self,name,contact_number,email,linkedin_urls,github_url,missing_sections,sections_not_capitalized,common_projects,section_order_suggestion,sections_text,skills,relevant_experience_score):
768
+ score = 0
769
+ if name:
770
+ name_parts = name.split()
771
+ num_parts = len(name_parts)
772
+
773
+ if num_parts == 0:
774
+ score += 0
775
+ if self.is_sentence_case(name):
776
+ score += 3
777
+ elif self.is_present_name(name):
778
+ score += 1.5
779
+
780
+ if contact_number and isinstance(contact_number, str):
781
+ digits_only = re.sub(r'\D', '', contact_number)
782
+
783
+ if digits_only.startswith("91") and len(digits_only) > 10:
784
+ digits_only = digits_only[2:] # Remove the first two characters ('91')
785
+
786
+ if len(digits_only) == 10 and digits_only[0] in "6789": # Check for valid Indian mobile numbers
787
+ score += 3
788
+
789
+ if email:
790
+ score += 3 if self.is_valid_email(email) else 0
791
+
792
+ score += 3 if linkedin_urls else 0
793
+
794
+ if github_url:
795
+ github_suggestion = self.is_valid_url(github_url)
796
+ score += 3 if not github_suggestion else 0
797
+ else:
798
+ score += 0
799
+
800
+ if len(missing_sections)==0 and len(sections_not_capitalized)==0:
801
+ score+=10
802
+ elif len(missing_sections)==0 and len(sections_not_capitalized)>0:
803
+ score+=8
804
+ elif len(missing_sections)<=3:
805
+ score+=6
806
+ elif len(missing_sections)>4:
807
+ score+=3
808
+
809
+ if common_projects:
810
+ score +=0
811
+ else:
812
+ score +=5
813
+
814
+ if section_order_suggestion:
815
+ score -= 2
816
+ else:
817
+ score
818
+
819
+ """
820
+ ds_skills_list_lower = [skill.lower() for skill in data_science_skills]
821
+ skills_present_lower = [skill.lower() for skill in self.extract_skills_from_resume(skills) ]
822
+
823
+ matching_skill_count = 0
824
+ for skill in skills_present_lower:
825
+ if ds_skills_list_lower:
826
+ matching_skill_count+=1
827
+ if matching_skill_count==0:
828
+ score+=0
829
+
830
+ if matching_skill_count<=5:
831
+ score+=2
832
+ elif matching_skill_count>=10 and matching_skill_count<=15:
833
+ score+5
834
+ else:
835
+ score+=8
836
+ """
837
+
838
+ if "PROJECTS" not in sections_text:
839
+ score+=0
840
+ else:
841
+ project_list = sections_text.get("PROJECTS",[])
842
+ project_count = len([x for x in project_list if "Description" in x])
843
+
844
+ if project_count<=2:
845
+ score+=2
846
+ elif project_count>2 and project_count<=4:
847
+ score+=5
848
+ elif project_count>4:
849
+ score+=3
850
+
851
+
852
+ resume_data = {}
853
+ # Extract projects & links
854
+ project_links = self.extract_project_links(sections_text)
855
+ projects_with_links = len(project_links)
856
+
857
+ # βœ… Count only projects with descriptions
858
+ valid_projects = [
859
+ p for p in sections_text.get("PROJECTS", []) if "description" in p.lower()
860
+ ]
861
+ total_projects = len(valid_projects) # βœ… Count projects properly
862
+
863
+ # βœ… Calculate project link score
864
+ project_link_score = self.calculate_project_link_score(projects_with_links)
865
+ resume_data["project_link_score"] = project_link_score
866
+
867
+ # βœ… Prevent division by zero
868
+ if total_projects > 0:
869
+ if projects_with_links == 0:
870
+ score += 0
871
+ elif projects_with_links / total_projects >= 0.5:
872
+ score += 1.5
873
+ if projects_with_links == total_projects:
874
+ score += 3
875
+ else:
876
+ score += 0 # βœ… Ensure no division error if no projects exist
877
+
878
+
879
+ """"
880
+ profile_summary = sections_text.get("PROFILE SUMMARY", "")
881
+ print(profile_summary)
882
+
883
+ summary_score = self.calculate_summary_score(profile_summary)
884
+ score += summary_score
885
+ """
886
+ ds_skills_score = self.calculate_ds_skills_score(skills)
887
+ score += ds_skills_score
888
+
889
+ certifications = sections_text.get("CERTIFICATIONS & ACADEMIC ENDEAVOURS", [])
890
+ num_certifications = len(certifications)
891
+
892
+ if num_certifications==0:
893
+ score+=0
894
+ elif 0 < num_certifications <= 2:
895
+ score+=3
896
+ elif 2 < num_certifications <= 4:
897
+ score+=5
898
+ elif num_certifications>4:
899
+ score+=7
900
+ """
901
+ extra_urls_bonus = self.calculate_extra_urls_bonus(pdf_path)
902
+ score += extra_urls_bonus
903
+ """
904
+ score += relevant_experience_score
905
+
906
+ score += project_link_score
907
+
908
+ return score
909
+
910
+
911
+ def imarticus_detailed_score(self, name, contact_number, email, linkedin_urls, github_url,
912
+ missing_sections=None, sections_not_capitalized=None, common_projects=None,
913
+ section_order_suggestion=None, sections_text=None, skills=None,
914
+ relevant_experience_score=0):
915
+
916
+ # Ensure lists and dictionaries have default values to avoid 'NoneType' errors
917
+ missing_sections = missing_sections or []
918
+ sections_not_capitalized = sections_not_capitalized or []
919
+ common_projects = common_projects or []
920
+ sections_text = sections_text or {}
921
+
922
+ score_breakdown = {
923
+ "name_score": 0,
924
+ "contact_number_score": 0,
925
+ "email_score": 0,
926
+ "linkedin_url_score": 0,
927
+ "github_url_score": 0,
928
+ "missing_sections_score": 0,
929
+ "common_projects_score": 0,
930
+ "section_order_score": 0,
931
+ "projects_score": 0,
932
+ "certifications_score": 0,
933
+ "relevant_experience_score": 0,
934
+ "ds_skills_score": 0,
935
+ "extra_urls_bonus": 0,
936
+ "summary_score": 0,
937
+ "project_link_score": 0
938
+ }
939
+
940
+ # βœ… Name Score (3 Points)
941
+ if name:
942
+ if self.is_sentence_case(name):
943
+ score_breakdown["name_score"] = 3
944
+ elif self.is_present_name(name):
945
+ score_breakdown["name_score"] = 1.5
946
+
947
+ # βœ… Contact Number Score (3 Points)
948
+ if contact_number and isinstance(contact_number, str):
949
+ digits_only = re.sub(r'\D', '', contact_number)
950
+ if digits_only.startswith("91") and len(digits_only) > 10:
951
+ digits_only = digits_only[2:]
952
+ if len(digits_only) == 10 and digits_only[0] in "6789":
953
+ score_breakdown["contact_number_score"] = 3
954
+
955
+ # βœ… Email Score (3 Points)
956
+ score_breakdown["email_score"] = 3 if email and self.is_valid_email(email) else 0
957
+
958
+ # βœ… LinkedIn URL Score (3 Points)
959
+ score_breakdown["linkedin_url_score"] = 3 if linkedin_urls else 0
960
+
961
+ # βœ… GitHub URL Score (3 Points)
962
+ if github_url and self.is_valid_url(github_url):
963
+ score_breakdown["github_url_score"] = 3
964
+
965
+ # βœ… Missing Sections Score (10 Points)
966
+ if not missing_sections and not sections_not_capitalized:
967
+ score_breakdown["missing_sections_score"] = 10
968
+ elif not missing_sections and sections_not_capitalized:
969
+ score_breakdown["missing_sections_score"] = 8
970
+ elif len(missing_sections) <= 3:
971
+ score_breakdown["missing_sections_score"] = 6
972
+ else:
973
+ score_breakdown["missing_sections_score"] = 3
974
+
975
+ # βœ… Common Projects Score (5 Points)
976
+ score_breakdown["common_projects_score"] = 0 if common_projects else 5
977
+
978
+ # βœ… Section Order Score (2 Points)
979
+ score_breakdown["section_order_score"] = -2 if section_order_suggestion else 0
980
+
981
+ # βœ… Projects Score (5 Points)
982
+ if "PROJECTS" in sections_text:
983
+ project_list = sections_text.get("PROJECTS", [])
984
+ project_count = len([x for x in project_list if "Description" in x])
985
+ if project_count <= 2:
986
+ score_breakdown["projects_score"] = 2
987
+ elif 2 < project_count <= 4:
988
+ score_breakdown["projects_score"] = 5
989
+ else:
990
+ score_breakdown["projects_score"] = 3
991
+
992
+ # βœ… Certifications Score (7 Points)
993
+ certifications = sections_text.get("CERTIFICATIONS & ACADEMIC ENDEAVOURS", [])
994
+ num_certifications = len(certifications)
995
+ if num_certifications == 0:
996
+ score_breakdown["certifications_score"] = 0
997
+ elif 0 < num_certifications <= 2:
998
+ score_breakdown["certifications_score"] = 3
999
+ elif 2 < num_certifications <= 4:
1000
+ score_breakdown["certifications_score"] = 5
1001
+ else:
1002
+ score_breakdown["certifications_score"] = 7
1003
+
1004
+ # βœ… Relevant Experience Score (5 Points)
1005
+ score_breakdown["relevant_experience_score"] = relevant_experience_score if relevant_experience_score is not None else 0
1006
+
1007
+ # βœ… Data Science Skills Score (5 Points)
1008
+ score_breakdown["ds_skills_score"] = self.calculate_ds_skills_score(skills)
1009
+
1010
+ # βœ… Extra URLs Bonus (5 Points)
1011
+ score_breakdown["extra_urls_bonus"] = self.calculate_extra_urls_bonus(sections_text)
1012
+
1013
+ # βœ… Summary Score (5 Points)
1014
+ profile_summary = sections_text.get("PROFILE SUMMARY", "")
1015
+ score_breakdown["summary_score"] = self.calculate_summary_score(profile_summary)
1016
+
1017
+ # βœ… Project Link Score (2 Points)
1018
+ project_links = self.extract_project_links(sections_text)
1019
+ projects_with_links = len(project_links)
1020
+ score_breakdown["project_link_score"] = self.calculate_project_link_score(projects_with_links)
1021
+
1022
+ return score_breakdown
1023
+
1024
+ def parse_text(self, path):
1025
+ logger = logging.getLogger(__name__)
1026
+ logging.getLogger("pdfminer").setLevel(logging.WARNING)
1027
+ resume_data = {}
1028
+ logger.debug('parsing text')
1029
+ text = self.extract_text_from_pdf(path)
1030
+ text1 = " ".join(text.split("\n"))
1031
+ skills_found = self.extract_skills_from_resume(text)
1032
+ found_keywords = self.extract_keyword_variations_from_resume(text)
1033
+ sections_text = self.segregate_sections(text)
1034
+ formatted_text = self.extract_and_format_sections(sections_text, Extract_sections)
1035
+ found_keyword_section = self.extract_keyword_variations_from_formatted_text(formatted_text)
1036
+
1037
+ parsed_sections = self.segregate_sections(text)
1038
+ projects = parsed_sections.get("PROJECTS", [])
1039
+ certifications = parsed_sections.get("CERTIFICATIONS & ACADEMIC ENDEAVOURS", [])
1040
+ projects_text = "\n".join(projects)
1041
+ certifications_text = "\n".join(certifications)
1042
+ found_imarticus_certification = self.check_imarticus_certifications(certifications_text)
1043
+ found_projects = self.check_common_projects(projects_text)
1044
+
1045
+ name, name_suggestion = self.extract_name(text)
1046
+ contact_number, contact_suggestion = self.extract_contact_number_from_resume(text)
1047
+ email, email_suggestion = self.extract_email_from_resume(path)
1048
+ github_urls = self.extract_github_urls_from_pdf(path)
1049
+ github_urls_suggestions = self.is_valid_url(github_urls)
1050
+ linkedin_urls = self.extract_linkedIn_urls_from_pdf(path)
1051
+ section_by_grammer_issues = self.grammar_issue_check(text, found_keyword_section, Extract_sections)
1052
+
1053
+
1054
+ domains = [
1055
+ r"hackerrank\.com", # Hackerrank
1056
+ r"leetcode\.com", # LeetCode
1057
+ r"medium\.com" # Medium
1058
+ ]
1059
+ extra_urls = self.extract_extra_urls_pdf(path, domains)
1060
+
1061
+ education_order_suggestion, education_suggestion = self.chronological_order_check(sections_text, "ACADEMIC PROFILE")
1062
+ experience_order_suggestion, experience_suggestion = self.chronological_order_check(sections_text, "WORK EXPERIENCE")
1063
+
1064
+ headers = list(sections_text.keys())
1065
+ spelling_suggestions = self.check_spelling(headers, section_headers)
1066
+
1067
+ predefined_terms = [name, email]
1068
+ predefined_terms.extend(required_sections)
1069
+ text_properties = self.extract_text_properties(path, predefined_terms)
1070
+ grouped_properties = self.group_similar_fonts(text_properties)
1071
+ different_texts = self.identify_different_fonts_and_sizes(grouped_properties)
1072
+
1073
+ font_suggestions = []
1074
+ for item in different_texts:
1075
+ font_suggestion = f"Formatting issue at Page: {item['page_num']}, Text: {item['text']}, Reason: {item['reason']}, Found font size: {item['found_size']}, Found font name: {item['found_font_name']}"
1076
+ font_suggestions.append(font_suggestion)
1077
+
1078
+ missing_sections, sections_not_capitalized = self.extract_sections_from_resume(text)
1079
+
1080
+ linkedin_urls_suggestion = str()
1081
+ common_project = str()
1082
+ if not name:
1083
+ name_suggestion = "Please add name to the resume."
1084
+ if not contact_number:
1085
+ contact_suggestion = "Please add the contact number to the resume."
1086
+ if not email:
1087
+ email_suggestion = "Please add the email address to the resume."
1088
+ if not github_urls:
1089
+ github_urls_suggestions = "add the github_urls to the resume."
1090
+ if not linkedin_urls:
1091
+ linkedin_urls_suggestion = "add the linkedin_urls to the resume."
1092
+ if found_projects:
1093
+ common_project = "Common projects found in Projects section: "
1094
+ for project in found_projects:
1095
+ common_project += project
1096
+
1097
+ # Replace the existing project length suggestion code with:
1098
+ project_list = sections_text.get("PROJECTS", [])
1099
+ projects_with_description = [
1100
+ p for p in project_list
1101
+ if "description" in p.lower()
1102
+ ]
1103
+ project_count = len(projects_with_description)
1104
+
1105
+ if project_count == 0:
1106
+ project_length_suggestion = "No projects found. Consider at least 2 projects."
1107
+ elif project_count == 1:
1108
+ project_length_suggestion = "Only 1 project found. Consider adding 1 more project."
1109
+ else:
1110
+ project_length_suggestion = f"{project_count} projects found."
1111
+
1112
+ # Store in resume data (keeps your existing URL extraction)
1113
+ resume_data["project_length_suggestion"] = project_length_suggestion
1114
+
1115
+ experience_text = sections_text.get("WORK EXPERIENCE", "") # βœ… Extract work experience section
1116
+ relevant_experience_score = self.calculate_relevant_experience_score(experience_text) # βœ… Calculate score
1117
+
1118
+ # βœ… Store in the final resume data output
1119
+ resume_data["relevant_experience_score"] = relevant_experience_score
1120
+
1121
+
1122
+ recommended_blogs = random.sample(blog_articles, 2)
1123
+ recommended_youtube = random.sample(youtube_links, 2)
1124
+
1125
+ # Calculate imarticus_score
1126
+ imarticus_score = self.imarticus_review_score(
1127
+ name,
1128
+ contact_number,
1129
+ email,
1130
+ linkedin_urls,
1131
+ github_urls,
1132
+ missing_sections,
1133
+ sections_not_capitalized,
1134
+ common_projects=found_projects, # Ensure to pass found projects
1135
+ section_order_suggestion=experience_order_suggestion,
1136
+ skills=skills_found, # Pass order suggestion
1137
+ sections_text=sections_text,
1138
+ relevant_experience_score=relevant_experience_score,
1139
+ # project_link_score=project_link_score
1140
+ #pdf_path=path
1141
+ #relevant_keywords_found=bool(found_keywords), # Convert to boolean
1142
+ #experience_orderly_arranged=experience_order_suggestion, # Pass orderly arrangement check
1143
+ #experience_section_present="WORK EXPERIENCE" in sections_text # Check if experience section is present
1144
+ )
1145
+
1146
+ # Populate resume data dictionary
1147
+ resume_data = {
1148
+ "name": name,
1149
+ "contact_number": contact_number,
1150
+ "email": email,
1151
+ "linkedin_urls": linkedin_urls,
1152
+ "experience_order_suggestion": experience_order_suggestion,
1153
+ "education_order_suggestion": education_order_suggestion,
1154
+ "grammer_issues_by_section": section_by_grammer_issues,
1155
+ "github_urls": github_urls,
1156
+ "skills": skills_found,
1157
+ "spelling_suggestions": spelling_suggestions,
1158
+ "found_keywords": found_keywords,
1159
+ "text": text,
1160
+ "font_suggestions": font_suggestions,
1161
+ "name_suggestion": name_suggestion,
1162
+ "contact_suggestion": contact_suggestion,
1163
+ "email_suggestion": email_suggestion,
1164
+ "imarticus_score": imarticus_score,
1165
+ "github_urls_suggestions": github_urls_suggestions,
1166
+ "linkedin_urls_suggestion": "Add the LinkedIn URLs to the resume." if not linkedin_urls else "",
1167
+ "missing_sections": missing_sections,
1168
+ "common_projects": "Common projects found in Projects section: " + ", ".join(found_projects) if found_projects else "",
1169
+ "project_length_suggestion": project_length_suggestion,
1170
+ "extra_urls": extra_urls,
1171
+ "certifications": {
1172
+ "found": found_imarticus_certification["found"],
1173
+ "message": found_imarticus_certification["message"],
1174
+ "text": certifications_text # Store extracted certification text
1175
+ },
1176
+ "recommended_blogs": recommended_blogs,
1177
+ "recommended_youtube_links": recommended_youtube
1178
+ }
1179
+
1180
+ # Additional checks and data additions
1181
+ if "WORK EXPERIENCE" in sections_text.keys() and "WORK EXPERIENCE" != list(sections_text.keys())[2]:
1182
+ section_order_suggestion = f"WORK EXPERIENCE should come before {list(sections_text.keys())[2]}"
1183
+ resume_data["section_order_suggestion"] = section_order_suggestion
1184
+
1185
+ missing_important_sections = self.check_missing_sections(resume_data)
1186
+ resume_data["basic_information_section"] = missing_important_sections or "Basic information is Found"
1187
+
1188
+ missing_skills = list(set(essential_skills) - set(skills_found))
1189
+ resume_data["missing_skills"] = missing_skills
1190
+
1191
+ found_keywords_count = len(resume_data["found_keywords"])
1192
+ num_keywords = len(keyword_variations)
1193
+ quality_mapping = {"Low": 0.2, "Medium": 0.5, "High": 0.8} # Assuming some quality mapping
1194
+ for quality, threshold in quality_mapping.items():
1195
+ if found_keywords_count < num_keywords * threshold:
1196
+ resume_data["quality"] = quality
1197
+ break
1198
+
1199
+ found_certification = "Imarticus certification found in Certifications section." if found_imarticus_certification else "No Imarticus certification found in Certifications section."
1200
+ resume_data["found_certification"] = found_certification
1201
+
1202
+ # Experience relevance check
1203
+ Extract_exp_sections = ['WORK EXPERIENCE']
1204
+ experience_text = self.extract_and_format_sections(sections_text, Extract_exp_sections)
1205
+ if experience_text:
1206
+ resume_data["work_experience_check"] = "Experience is relevant to Data science." if any(variation.lower() in experience_text.lower() for keyword, variations in keyword_variations.items() for variation in variations) else "Experience is not relevant to Data science."
1207
+
1208
+ return jsonify(resume_data)
src/reviewer.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import language_tool_python
2
+ import re
3
+
4
+ class ResumeReviewer:
5
+ def __init__(self):
6
+ self.tool = language_tool_python.LanguageTool('en-US')
7
+
8
+ def grammar_check(self, parsed_resume):
9
+ if not parsed_resume:
10
+ return {}, 3 # βœ… Return empty dictionary and high grammar score
11
+
12
+ section_wise_issues = {}
13
+ total_grammar_errors = 0
14
+ total_words = 0
15
+ grammar_score = 0
16
+
17
+ ignore_keywords = {"imarticus", "github", "linkedin", "microsoft office", "google drive", "amazon web services"}
18
+ ignore_rule_ids = {"WHITESPACE_RULE", "EN_QUOTES", "CONSECUTIVE_SPACES", "UPPERCASE_SENTENCE_START"} # βœ… Ignore false uppercase errors
19
+
20
+ ignore_patterns = [
21
+ r'\d+', # Ignore standalone numbers
22
+ r'https?://\S+', # Ignore URLs
23
+ r'\S+@\S+', # Ignore emails
24
+ r'^\s*[-β€’]+\s*', # βœ… Ignore bullet points
25
+ ]
26
+
27
+ name_to_exclude = parsed_resume.get("name", "").strip().lower()
28
+
29
+ for section, text in parsed_resume.items():
30
+ if isinstance(text, str) and text.strip():
31
+ words_in_section = len(text.split())
32
+ total_words += words_in_section
33
+
34
+ matches = self.tool.check(text)
35
+ grammar_issues = []
36
+ spelling_errors = []
37
+
38
+ for match in matches:
39
+ context_text = match.context.lower()
40
+
41
+ # βœ… Ignore rule-based exclusions
42
+ if match.ruleId in ignore_rule_ids or any(keyword in context_text for keyword in ignore_keywords):
43
+ continue
44
+ if name_to_exclude and name_to_exclude in context_text:
45
+ continue
46
+ if any(re.search(pattern, match.context) for pattern in ignore_patterns):
47
+ continue
48
+
49
+ # βœ… Ignore false uppercase errors caused by bullet points
50
+ if "sentence does not start with an uppercase letter" in match.message.lower() and re.search(r'^\s*[-β€’]+\s*', match.context):
51
+ continue
52
+
53
+ issue = {
54
+ "context": match.context,
55
+ "error": match.message,
56
+ "rule_id": match.ruleId,
57
+ "suggested_correction": match.replacements
58
+ }
59
+
60
+ # βœ… Separate grammar and spelling errors
61
+ if "SPELL" in issue.get("rule_id", ""):
62
+ spelling_errors.append(issue)
63
+ else:
64
+ grammar_issues.append(issue)
65
+ total_grammar_errors += 1
66
+
67
+ if grammar_issues or spelling_errors:
68
+ section_wise_issues[section] = {
69
+ "grammar_issues": grammar_issues,
70
+ "spelling_errors": spelling_errors
71
+ }
72
+
73
+ # βœ… Calculate mistake percentage
74
+ mistake_percentage = (total_grammar_errors / total_words) * 100 if total_words > 0 else 0
75
+
76
+ # βœ… Apply scoring logic from Excel
77
+ if total_grammar_errors == 0:
78
+ grammar_score = 3
79
+ elif mistake_percentage < 50:
80
+ grammar_score = 2
81
+ else:
82
+ grammar_score = 1
83
+
84
+ return section_wise_issues, grammar_score # βœ… Always returns 2 values
src/submitter.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from .config import UPLOAD_FOLDER, ALLOWED_EXTENSIONS
3
+ from flask import render_template, redirect, flash, request, url_for
4
+ from werkzeug.utils import secure_filename
5
+
6
+ class ResumeSubmitter:
7
+ def __init__(self):
8
+ if not os.path.exists(UPLOAD_FOLDER):
9
+ os.makedirs(UPLOAD_FOLDER)
10
+
11
+ def allowed_file(self, filename):
12
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
13
+
14
+ def upload_form(self):
15
+ return render_template("upload_resume.html")
16
+
17
+ def upload_file(self):
18
+ if 'file' not in request.files:
19
+ return 'No file part'
20
+
21
+ file = request.files['file']
22
+ if file.filename == '':
23
+ return 'No selected file'
24
+
25
+ if file and self.allowed_file(file.filename):
26
+ filename = secure_filename(file.filename)
27
+ file.save(os.path.join(UPLOAD_FOLDER, filename))
28
+ flash('File successfully uploaded')
29
+ # return file path
30
+ return os.path.join(UPLOAD_FOLDER, filename)
31
+ else:
32
+ return "Allowed file types are PDF as of now"