WebashalarForML commited on
Commit
be28faf
1 Parent(s): 8b536a2

Upload 5 files

Browse files
Files changed (5) hide show
  1. BeckUp.py +177 -0
  2. app.py +115 -0
  3. app_error.log +1 -0
  4. readme +146 -0
  5. requirements.txt +148 -0
BeckUp.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, redirect, flash, session, render_template, url_for
2
+ import os
3
+ import json
4
+ from werkzeug.utils import secure_filename
5
+ import logging
6
+ from utils.error import handle_file_not_found, handle_invalid_file_type, handle_file_processing_error, page_not_found, internal_server_error
7
+ from utils.spacy import Parser_from_model
8
+ from utils.mistral import process_resume_data
9
+ import platform
10
+ from waitress import serve
11
+
12
+ if platform.system() == "Windows":
13
+ app = Flask(__name__)
14
+ app.secret_key = 'your_secret_key'
15
+ app.config['UPLOAD_FOLDER'] = 'uploads'
16
+ # else:
17
+ # # For Hugging Face Spaces or other Linux environments
18
+ # if __name__ != "__main__":
19
+ # serve(app, host="0.0.0.0", port=7860)
20
+
21
+
22
+ # Error handlers
23
+ app.register_error_handler(404, page_not_found)
24
+ app.register_error_handler(500, internal_server_error)
25
+
26
+ # Allowed extensions
27
+ ALLOWED_EXTENSIONS = {'pdf', 'docx', 'rsf', 'odt', 'png', 'jpg', 'jpeg'}
28
+
29
+ def allowed_file(filename):
30
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
31
+
32
+ @app.route('/')
33
+ def index():
34
+ uploaded_file = session.get('uploaded_file', None)
35
+ return render_template('index.html', uploaded_file=uploaded_file)
36
+
37
+ # @app.route('/upload', methods=['POST'])
38
+ # def upload_file():
39
+ # if 'file' not in request.files:
40
+ # flash('No file part')
41
+ # return redirect(request.url)
42
+
43
+ # file = request.files['file']
44
+
45
+ # if file.filename == '':
46
+ # flash('No selected file')
47
+ # return redirect(request.url)
48
+
49
+ # if file and allowed_file(file.filename):
50
+ # filename = secure_filename(file.filename)
51
+ # file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
52
+ # logging.debug(f"File uploaded: {filename}")
53
+ # session['uploaded_file'] = filename
54
+ # flash('File successfully uploaded')
55
+ # return redirect(url_for('index'))
56
+ # else:
57
+ # return handle_invalid_file_type()
58
+
59
+ # def process_file():
60
+ # selected_file = session.get('uploaded_file')
61
+ # if not selected_file:
62
+ # flash('No file selected for processing')
63
+ # return redirect(url_for('index'))
64
+
65
+ # file_path = os.path.join(app.config['UPLOAD_FOLDER'], selected_file)
66
+ # if not os.path.exists(file_path):
67
+ # return handle_file_not_found()
68
+ # parsed_data = process_resume_data(file_path)
69
+ # if not parsed_data or 'error' in parsed_data:
70
+ # return handle_file_processing_error()
71
+
72
+ # session['processed_data'] = parsed_data
73
+ # flash('Data processed successfully')
74
+ # return redirect(url_for('result'))
75
+
76
+ @app.route('/upload_and_process', methods=['POST', 'GET'])
77
+ def upload_and_process():
78
+ if 'file' not in request.files:
79
+ flash('No file part')
80
+ return redirect(request.url)
81
+
82
+ file = request.files['file']
83
+
84
+ if file.filename == '':
85
+ flash('No selected file')
86
+ return redirect(request.url)
87
+
88
+ if file and allowed_file(file.filename):
89
+ filename = secure_filename(file.filename)
90
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
91
+ file.save(file_path)
92
+ logging.debug(f"File uploaded: {filename}")
93
+ session['uploaded_file'] = filename
94
+
95
+ # Process the file after uploading
96
+ parsed_data = process_resume_data(file_path)
97
+ if not parsed_data or 'error' in parsed_data:
98
+ return handle_file_processing_error()
99
+
100
+ session['processed_data'] = parsed_data
101
+ flash('File uploaded and data processed successfully')
102
+ return redirect(url_for('result'))
103
+ else:
104
+ return handle_invalid_file_type()
105
+
106
+
107
+
108
+ @app.route('/remove_file')
109
+ def remove_file():
110
+ uploaded_file = session.get('uploaded_file')
111
+ if uploaded_file:
112
+ os.remove(os.path.join(app.config['UPLOAD_FOLDER'], uploaded_file))
113
+ session.pop('uploaded_file', None)
114
+ flash('File successfully removed')
115
+ return redirect(url_for('index'))
116
+
117
+ @app.route('/reset_upload')
118
+ def reset_upload():
119
+ uploaded_file = session.get('uploaded_file')
120
+ if uploaded_file:
121
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_file)
122
+ if os.path.exists(file_path):
123
+ os.remove(file_path)
124
+
125
+ session.pop('uploaded_file', None)
126
+
127
+ session.pop('processed_data', None)
128
+ flash('File reset. You can upload a new file.')
129
+ return redirect(url_for('index'))
130
+
131
+ # @app.route('/process', methods=['GET', 'POST'])
132
+ # def process_file():
133
+ # selected_file = session.get('uploaded_file')
134
+ # if not selected_file:
135
+ # flash('No file selected for processing')
136
+ # return redirect(url_for('index'))
137
+
138
+ # file_path = os.path.join(app.config['UPLOAD_FOLDER'], selected_file)
139
+ # if not os.path.exists(file_path):
140
+ # return handle_file_not_found()
141
+ # parsed_data = process_resume_data(file_path)
142
+ # if not parsed_data or 'error' in parsed_data:
143
+ # return handle_file_processing_error()
144
+
145
+ # session['processed_data'] = parsed_data
146
+ # flash('Data processed successfully')
147
+ # return redirect(url_for('result'))
148
+
149
+ @app.route('/loading')
150
+ def loading():
151
+ selected_file = session.get('uploaded_file')
152
+ if not selected_file:
153
+ flash('No file selected for processing')
154
+ return redirect(url_for('index'))
155
+
156
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], selected_file)
157
+
158
+ parsed_data = process_resume_data(file_path)
159
+
160
+ if parsed_data and 'error' not in parsed_data:
161
+ session['processed_data'] = json.loads(parsed_data)
162
+ flash('Data processed successfully')
163
+ return redirect(url_for('result'))
164
+ else:
165
+ return handle_file_processing_error()
166
+
167
+ @app.route('/result')
168
+ def result():
169
+ processed_data = session.get('processed_data', None)
170
+ if not processed_data:
171
+ flash('No data to display. Please upload and process a file.')
172
+ return redirect(url_for('index'))
173
+
174
+ return render_template('result.html', parsed_data=processed_data)
175
+
176
+ if __name__ == '__main__':
177
+ app.run(debug=True)
app.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, redirect, flash, session, render_template, url_for
2
+ import os
3
+ import json
4
+ import logging
5
+ from werkzeug.utils import secure_filename
6
+ from utils.error import handle_file_not_found, handle_invalid_file_type, handle_file_processing_error, page_not_found, internal_server_error
7
+ from utils.spacy import Parser_from_model
8
+ from utils.mistral import process_resume_data
9
+ import platform
10
+ from waitress import serve
11
+
12
+ # Initialize the Flask application
13
+ app = Flask(__name__)
14
+ app.secret_key = 'your_secret_key'
15
+ app.config['UPLOAD_FOLDER'] = 'uploads'
16
+
17
+ # Allowed file extensions
18
+ ALLOWED_EXTENSIONS = {'pdf', 'docx', 'rsf', 'odt', 'png', 'jpg', 'jpeg'}
19
+
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.DEBUG)
22
+
23
+ # Error handlers
24
+ app.register_error_handler(404, page_not_found)
25
+ app.register_error_handler(500, internal_server_error)
26
+
27
+ def allowed_file(filename):
28
+ """Check if the file has an allowed extension."""
29
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
30
+
31
+ @app.route('/')
32
+ def index():
33
+ """Display the index page with the uploaded file information."""
34
+ uploaded_file = session.get('uploaded_file', None)
35
+ return render_template('index.html', uploaded_file=uploaded_file)
36
+
37
+ @app.route('/upload_and_process', methods=['POST'])
38
+ def upload_and_process():
39
+ """Handle file upload and process the file."""
40
+ if 'file' not in request.files or request.files['file'].filename == '':
41
+ flash('No file selected for upload.')
42
+ return redirect(request.url)
43
+
44
+ file = request.files['file']
45
+
46
+ # Check if the file is allowed
47
+ if file and allowed_file(file.filename):
48
+ filename = secure_filename(file.filename)
49
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
50
+ file.save(file_path)
51
+ logging.debug(f"File uploaded: {filename}")
52
+ session['uploaded_file'] = filename
53
+
54
+ # Process the file after uploading
55
+ try:
56
+ parsed_data = process_resume_data(file_path)
57
+ if not parsed_data or 'error' in parsed_data:
58
+ flash('An error occurred during file processing.')
59
+ return redirect(url_for('index'))
60
+
61
+ session['processed_data'] = parsed_data
62
+ flash('File uploaded and data processed successfully.')
63
+ return redirect(url_for('result'))
64
+
65
+ except Exception as e:
66
+ logging.error(f"File processing error: {str(e)}")
67
+ flash('An error occurred while processing the file.')
68
+ return handle_file_processing_error()
69
+ else:
70
+ return handle_invalid_file_type()
71
+
72
+ @app.route('/remove_file', methods=['POST'])
73
+ def remove_file():
74
+ """Remove the uploaded file and reset the session."""
75
+ uploaded_file = session.get('uploaded_file')
76
+ if uploaded_file:
77
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_file)
78
+ if os.path.exists(file_path):
79
+ os.remove(file_path)
80
+ session.pop('uploaded_file', None)
81
+ flash('File successfully removed.')
82
+ else:
83
+ flash('No file to remove.')
84
+ return redirect(url_for('index'))
85
+
86
+ @app.route('/reset_upload')
87
+ def reset_upload():
88
+ """Reset the uploaded file and the processed data."""
89
+ uploaded_file = session.get('uploaded_file')
90
+ if uploaded_file:
91
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_file)
92
+ if os.path.exists(file_path):
93
+ os.remove(file_path)
94
+ session.pop('uploaded_file', None)
95
+
96
+ session.pop('processed_data', None)
97
+ flash('File and data reset. You can upload a new file.')
98
+ return redirect(url_for('index'))
99
+
100
+ @app.route('/result')
101
+ def result():
102
+ """Display the processed data result."""
103
+ processed_data = session.get('processed_data', None)
104
+ if not processed_data:
105
+ flash('No data to display. Please upload and process a file.')
106
+ return redirect(url_for('index'))
107
+ return render_template('result.html', parsed_data=processed_data)
108
+
109
+ if __name__ == '__main__':
110
+ # For Windows development
111
+ if platform.system() == "Windows":
112
+ app.run(debug=True)
113
+ # For Linux or production with Waitress
114
+ else:
115
+ serve(app, host="0.0.0.0", port=7860)
app_error.log ADDED
@@ -0,0 +1 @@
 
 
1
+ 2024-09-28 13:31:17,959 ERROR: 404 Error: http://127.0.0.1:5000/favicon.ico
readme ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_
2
+ _\\----------- **Resume Parser** ----------\\_
3
+ _\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_
4
+
5
+ # Overview:
6
+ This project is a comprehensive Resume Parsing tool built using Python,
7
+ integrating the Mistral-Nemo-Instruct-2407 model for primary parsing.
8
+ If Mistral fails or encounters issues,
9
+ the system falls back to a custom-trained spaCy model to ensure continued functionality.
10
+ The tool is wrapped with a Flask API and has a user interface built using HTML and CSS.
11
+
12
+
13
+ # Installation Guide:
14
+
15
+ 1. Create and Activate a Virtual Environment
16
+ python -m venv venv
17
+ source venv/bin/activate # For Linux/Mac
18
+ # or
19
+ venv\Scripts\activate # For Windows
20
+
21
+ # NOTE: If the virtual environment (venv) is already created, you can skip the creation step and just activate.
22
+ - For Linux/Mac:
23
+ source venv/bin/activate
24
+ - For Windows:
25
+ venv\Scripts\activate
26
+
27
+ 2. Install Required Libraries
28
+ pip install -r requirements.txt
29
+
30
+ # Ensure the following dependencies are included:
31
+ - Flask
32
+ - spaCy
33
+ - huggingface_hub
34
+ - PyMuPDF
35
+ - python-docx
36
+ - Tesseract-OCR (for image-based parsing)
37
+
38
+ 3. Set up Hugging Face Token
39
+ - Add your Hugging Face token to the .env file as:
40
+ HF_TOKEN=<your_huggingface_token>
41
+
42
+
43
+ # File Structure Overview:
44
+ Mistral_With_Spacy/
45
+
46
+ ├── Spacy_Models/
47
+ │ └── ner_model_05_3 # Pretrained spaCy model directory for resume parsing
48
+
49
+ ├── templates/
50
+ │ ├── index.html # UI for file upload
51
+ │ └── result.html # Display parsed results in structured JSON
52
+
53
+ ├── uploads/ # Directory for uploaded resume files
54
+
55
+ ├── utils/
56
+ │ ├── mistral.py # Code for calling Mistral API and handling responses
57
+ │ ├── spacy.py # spaCy fallback model for parsing resumes
58
+ │ ├── error.py # Error handling utilities
59
+ │ └── fileTotext.py # Functions to extract text from different file formats (PDF, DOCX, etc.)
60
+
61
+ ├── venv/ # Virtual environment
62
+
63
+ ├── .env # Environment variables file (contains Hugging Face token)
64
+
65
+ ├── main.py # Flask app handling API routes for uploading and processing resumes
66
+
67
+ └── requirements.txt # Dependencies required for the project
68
+
69
+
70
+ # Program Overview:
71
+
72
+ # Mistral Integration (utils/mistral.py)
73
+ - Mistral API Calls: Uses Hugging Face’s Mistral-Nemo-Instruct-2407 model to parse resumes.
74
+ - Personal and Professional Extraction: Two functions extract personal and professional information in structured JSON format.
75
+ - Fallback Mechanism: If Mistral fails, spaCy NER model is used as a fallback.
76
+
77
+ # SpaCy Integration (utils/spacy.py)
78
+ - Custom Trained Model: Uses a spaCy model (ner_model_05_3) trained specifically for resume parsing.
79
+ - Named Entity Recognition: Extracts key information like Name, Email, Contact, Location, Skills, Experience, etc., from resumes.
80
+ - Validation: Includes validation for extracted emails and contacts.
81
+
82
+ # File Conversion (utils/fileTotext.py)
83
+ - Text Extraction: Handles different resume formats (PDF, DOCX, ODT, RSF, and images like PNG, JPG, JPEG) and extracts text for further processing.
84
+ - PDF Files: Uses PyMuPDF to extract text and, if necessary, Tesseract-OCR for image-based PDF content.
85
+ - DOCX Files: Uses `python-docx` to extract structured text from Word documents.
86
+ - ODT Files: Uses `odfpy` to extract text from ODT (OpenDocument) files.
87
+ - RSF Files: Reads plain text from RSF files.
88
+ - Images (PNG, JPG, JPEG): Uses Tesseract-OCR to extract text from image-based resumes.
89
+
90
+ - Hyperlink Extraction: Extracts hyperlinks from PDF files, capturing any embedded URLs during the parsing process.
91
+
92
+
93
+ # Error Handling (utils/error.py)
94
+ - Handles API response errors, file format errors, and ensures smooth fallbacks without crashing the app.
95
+
96
+ # Flask API (main.py)
97
+ Endpoints:
98
+ - /upload for uploading resumes.
99
+ - Displays parsed results in JSON format on the results page.
100
+ - UI: Simple interface for uploading resumes and viewing the parsing results.
101
+
102
+
103
+ # Tree map of your program:
104
+
105
+ main.py
106
+ ├── Handles API side
107
+ ├── File upload/remove
108
+ ├── Process resumes
109
+ └── Show result
110
+
111
+ utils
112
+ ├── fileTotext.py
113
+ │ └── Converts files to text
114
+ │ ├── PDF
115
+ │ ├── DOCX
116
+ │ ├── RTF
117
+ │ ├── ODT
118
+ │ ├── PNG
119
+ │ ├── JPG
120
+ │ └── JPEG
121
+ ├── mistral.py
122
+ │ ├── Mistral API Calls
123
+ │ │ └── Uses Mistral-Nemo-Instruct-2407 model
124
+ │ ├── Personal and Professional Extraction
125
+ │ │ ├── Extracts personal information
126
+ │ │ └── Extracts professional information
127
+ │ └── Fallback Mechanism
128
+ │ └── Uses spaCy NER model if Mistral fails
129
+ └── spacy.py
130
+ ├── Custom Trained Model
131
+ │ └── Uses spaCy model (ner_model_05_3)
132
+ ├── Named Entity Recognition
133
+ │ └── Extracts key information (Name, Email, Contact, etc.)
134
+ └── Validation
135
+ └── Validates emails and contacts
136
+
137
+
138
+ # References:
139
+
140
+ - [Flask Documentation](https://flask.palletsprojects.com/)
141
+ - [spaCy Documentation](https://spacy.io/usage)
142
+ - [Hugging Face Hub API](https://huggingface.co/docs/huggingface_hub/index)
143
+ - [PyMuPDF (MuPDF) Documentation](https://pymupdf.readthedocs.io/en/latest/)
144
+ - [python-docx Documentation](https://python-docx.readthedocs.io/en/latest/)
145
+ - [Tesseract OCR Documentation](https://github.com/tesseract-ocr/tesseract)
146
+ - [Virtual Environments in Python](https://docs.python.org/3/tutorial/venv.html)
requirements.txt ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ alembic==1.13.2
2
+ amqp==5.2.0
3
+ annotated-types==0.7.0
4
+ attrs==24.2.0
5
+ bcrypt==4.2.0
6
+ blinker==1.8.2
7
+ blis==0.7.11
8
+ cachetools==5.5.0
9
+ catalogue==2.0.10
10
+ certifi==2024.8.30
11
+ cffi==1.17.1
12
+ charset-normalizer==3.3.2
13
+ click==8.1.7
14
+ cloudpathlib==0.19.0
15
+ colorama==0.4.6
16
+ confection==0.1.5
17
+ croniter==3.0.3
18
+ cryptography==43.0.1
19
+ cymem==2.0.8
20
+ debtcollector==3.0.0
21
+ decorator==5.1.1
22
+ defusedxml==0.7.1
23
+ dnspython==2.6.1
24
+ dogpile.cache==1.3.3
25
+ eventlet==0.37.0
26
+ fasteners==0.19
27
+ filelock==3.16.0
28
+ Flask==3.0.3
29
+ fsspec==2024.9.0
30
+ futurist==3.0.0
31
+ greenlet==3.1.0
32
+ huggingface-hub==0.24.7
33
+ idna==3.8
34
+ importlib_metadata==8.5.0
35
+ iso8601==2.1.0
36
+ itsdangerous==2.2.0
37
+ Jinja2==3.1.4
38
+ jsonschema==4.23.0
39
+ jsonschema-specifications==2023.12.1
40
+ keystoneauth1==5.8.0
41
+ keystonemiddleware==10.7.1
42
+ kombu==5.4.1
43
+ langcodes==3.4.0
44
+ language_data==1.2.0
45
+ logutils==0.3.5
46
+ lxml==5.3.0
47
+ Mako==1.3.5
48
+ marisa-trie==1.2.0
49
+ markdown-it-py==3.0.0
50
+ MarkupSafe==2.1.5
51
+ mdurl==0.1.2
52
+ mistral==18.0.1
53
+ mistral-lib==3.0.0
54
+ msgpack==1.1.0
55
+ murmurhash==1.0.10
56
+ netaddr==1.3.0
57
+ netifaces==0.11.0
58
+ networkx==3.3
59
+ numpy==1.26.4
60
+ odfpy==1.4.1
61
+ os-service-types==1.7.0
62
+ oslo.cache==3.8.0
63
+ oslo.concurrency==6.1.0
64
+ oslo.config==9.6.0
65
+ oslo.context==5.6.0
66
+ oslo.db==16.0.0
67
+ oslo.i18n==6.4.0
68
+ oslo.log==6.1.2
69
+ oslo.messaging==14.9.0
70
+ oslo.metrics==0.9.0
71
+ oslo.middleware==6.2.0
72
+ oslo.policy==4.4.0
73
+ oslo.serialization==5.5.0
74
+ oslo.service==3.5.0
75
+ oslo.utils==7.3.0
76
+ osprofiler==4.2.0
77
+ packaging==24.1
78
+ paramiko==3.4.1
79
+ Paste==3.10.1
80
+ PasteDeploy==3.1.0
81
+ pbr==6.1.0
82
+ pdf2image==1.17.0
83
+ pecan==1.5.1
84
+ pillow==10.4.0
85
+ ply==3.11
86
+ preshed==3.0.9
87
+ prettytable==3.11.0
88
+ prometheus_client==0.20.0
89
+ pycadf==3.1.1
90
+ pycparser==2.22
91
+ pydantic==2.9.1
92
+ pydantic_core==2.23.3
93
+ Pygments==2.18.0
94
+ PyJWT==2.9.0
95
+ PyMuPDF==1.24.10
96
+ PyMuPDFb==1.24.10
97
+ PyNaCl==1.5.0
98
+ pyparsing==3.1.4
99
+ pytesseract==0.3.13
100
+ python-dateutil==2.9.0.post0
101
+ python-docx==1.1.2
102
+ python-dotenv==1.0.1
103
+ python-keystoneclient==5.5.0
104
+ pytz==2024.2
105
+ PyYAML==6.0.2
106
+ referencing==0.35.1
107
+ repoze.lru==0.7
108
+ requests==2.32.3
109
+ rfc3986==2.0.0
110
+ rich==13.8.1
111
+ Routes==2.5.1
112
+ rpds-py==0.20.0
113
+ setuptools==74.1.2
114
+ shellingham==1.5.4
115
+ simplegeneric==0.8.1
116
+ six==1.16.0
117
+ smart-open==7.0.4
118
+ spacy==3.8.0
119
+ spacy-legacy==3.0.12
120
+ spacy-loggers==1.0.5
121
+ SQLAlchemy==2.0.34
122
+ srsly==2.4.8
123
+ statsd==4.0.1
124
+ stevedore==5.3.0
125
+ tenacity==9.0.0
126
+ testresources==2.0.1
127
+ testscenarios==0.5.0
128
+ testtools==2.7.2
129
+ thinc==8.2.5
130
+ tooz==6.3.0
131
+ tqdm==4.66.5
132
+ typer==0.12.5
133
+ typing_extensions==4.12.2
134
+ tzdata==2024.1
135
+ urllib3==2.2.3
136
+ vine==5.1.0
137
+ voluptuous==0.15.2
138
+ waitress==3.0.0
139
+ wasabi==1.1.3
140
+ wcwidth==0.2.13
141
+ weasel==0.4.1
142
+ WebOb==1.8.8
143
+ Werkzeug==3.0.4
144
+ wrapt==1.16.0
145
+ WSME==0.12.1
146
+ yappi==1.6.0
147
+ yaql==3.0.0
148
+ zipp==3.20.1