From aac50e445c2f510ede4328af70a22694c96f60bb Mon Sep 17 00:00:00 2001 From: Hrishikesh Uchake Date: Sat, 24 Jan 2026 20:01:09 -0600 Subject: [PATCH] Remove LinkedIn Profile URL section from UI, backend, validation, and documentation --- app.py | 26 +++++-------------- app_production.py | 38 +++++---------------------- database/mongodb.py | 2 -- docs/ARCHITECTURE.md | 11 ++------ docs/MONGODB_GUIDE.md | 1 - docs/QUICK_START.md | 1 - docs/README.md | 8 +++--- generator/resume_generator.py | 2 -- scrapers/linkedin_scraper.py | 49 ----------------------------------- tests/test_comprehensive.py | 38 ++------------------------- utils/validators.py | 38 +++------------------------ 11 files changed, 26 insertions(+), 188 deletions(-) diff --git a/app.py b/app.py index c01f783..1a3f4fb 100644 --- a/app.py +++ b/app.py @@ -143,13 +143,7 @@
- - -
Example: https://www.linkedin.com/in/satyanadella/
-
- -
- +
Settings > Data Privacy > Get a copy of your data > Download ZIP
@@ -174,13 +168,12 @@ const status = document.getElementById('status'); const github = document.getElementById('github').value; - const linkedin = document.getElementById('linkedin').value; const linkedinFile = document.getElementById('linkedinFile').files[0]; const jobDescription = document.getElementById('jobDescription').value; - if (!github && !linkedin && !linkedinFile) { + if (!github && !linkedinFile) { status.className = 'status error'; - status.textContent = 'Please provide at least one profile (GitHub, LinkedIn URL, or Data Export)'; + status.textContent = 'Please provide at least one profile (GitHub username or LinkedIn Data Export)'; return; } @@ -200,7 +193,6 @@ if (linkedinFile) { const formData = new FormData(); formData.append('github_username', github); - formData.append('linkedin_url', linkedin); formData.append('job_description', jobDescription); formData.append('linkedin_data', linkedinFile); @@ -216,7 +208,6 @@ }, body: JSON.stringify({ github_username: github, - linkedin_url: linkedin, job_description: jobDescription }) }); @@ -265,26 +256,24 @@ def generate_resume(): Main endpoint to generate a tailored resume Expected payload: - - JSON: {"github_username": "...", "linkedin_url": "...", "job_description": "..."} - - OR Multipart: form fields github_username, linkedin_url, job_description AND optional file linkedin_data + - JSON: {"github_username": "...", "job_description": "..."} + - OR Multipart: form fields github_username, job_description AND optional file linkedin_data """ try: linkedin_file = None if request.content_type and "multipart/form-data" in request.content_type: github_username = request.form.get("github_username", "").strip() - linkedin_url = request.form.get("linkedin_url", "").strip() job_description = request.form.get("job_description", "").strip() linkedin_file = request.files.get("linkedin_data") else: data = request.json github_username = data.get("github_username", "").strip() - linkedin_url = data.get("linkedin_url", "").strip() job_description = data.get("job_description", "").strip() if not job_description: return jsonify({"error": "Job description is required"}), 400 - if not github_username and not linkedin_url and not linkedin_file: + if not github_username and not linkedin_file: return ( jsonify({"error": "At least one profile or data export is required"}), 400, @@ -312,9 +301,6 @@ def generate_resume(): # Cleanup if os.path.exists(file_path): os.remove(file_path) - elif linkedin_url: - linkedin_data = linkedin_scraper.scrape_profile(linkedin_url) - profile_data["linkedin"] = linkedin_data # Step 2: Analyze job description job_requirements = job_analyzer.analyze(job_description) diff --git a/app_production.py b/app_production.py index d2d4e39..ccd8e4c 100644 --- a/app_production.py +++ b/app_production.py @@ -193,13 +193,7 @@
- - -
Example: https://www.linkedin.com/in/satyanadella/
-
- -
- +
Settings > Data Privacy > Get a copy of your data > Download ZIP
@@ -224,13 +218,12 @@ const status = document.getElementById('status'); const github = document.getElementById('github').value; - const linkedin = document.getElementById('linkedin').value; const linkedinFile = document.getElementById('linkedinFile').files[0]; const jobDescription = document.getElementById('jobDescription').value; - if (!github && !linkedin && !linkedinFile) { + if (!github && !linkedinFile) { status.className = 'status error'; - status.textContent = 'Please provide at least one profile (GitHub, LinkedIn URL, or Data Export)'; + status.textContent = 'Please provide at least one profile (GitHub username or LinkedIn Data Export)'; return; } @@ -250,7 +243,6 @@ if (linkedinFile) { const formData = new FormData(); formData.append('github_username', github); - formData.append('linkedin_url', linkedin); formData.append('job_description', jobDescription); formData.append('linkedin_data', linkedinFile); @@ -266,7 +258,6 @@ }, body: JSON.stringify({ github_username: github, - linkedin_url: linkedin, job_description: jobDescription }) }); @@ -383,8 +374,8 @@ def generate_resume(): Main endpoint to generate a tailored resume Expected payload: - - JSON: {"github_username": "...", "linkedin_url": "...", "job_description": "..."} - - OR Multipart: form fields github_username, linkedin_url, job_description AND optional file linkedin_data + - JSON: {"github_username": "...", "job_description": "..."} + - OR Multipart: form fields github_username, job_description AND optional file linkedin_data Returns: PDF file: application/pdf @@ -395,7 +386,6 @@ def generate_resume(): if request.content_type and "multipart/form-data" in request.content_type: github_username = request.form.get("github_username", "").strip() - linkedin_url = request.form.get("linkedin_url", "").strip() job_description = request.form.get("job_description", "").strip() linkedin_file = request.files.get("linkedin_data") else: @@ -404,7 +394,6 @@ def generate_resume(): raise InvalidJobDescription("Request body is empty") github_username = data.get("github_username", "").strip() - linkedin_url = data.get("linkedin_url", "").strip() job_description = data.get("job_description", "").strip() logger.info(f"[{request.request_id}] Resume generation request") @@ -413,18 +402,16 @@ def generate_resume(): ( github_username, job_description, - linkedin_url, ) = InputValidator.validate_request( - github_username, job_description, linkedin_url + github_username, job_description ) if ( not github_username - and not linkedin_url and not (linkedin_file and linkedin_file.filename) ): raise ValidationError( - "Please provide at least one profile source (GitHub Username, LinkedIn URL, or LinkedIn Data Export zip file)" + "Please provide at least one profile source (GitHub Username or LinkedIn Data Export zip file)" ) logger.info(f"[{request.request_id}] Input validation passed") @@ -471,17 +458,6 @@ def generate_resume(): logger.warning( f"[{request.request_id}] LinkedIn export parsing failed: {e}" ) - elif linkedin_url: - try: - logger.debug(f"[{request.request_id}] Scraping LinkedIn profile") - linkedin_data = linkedin_scraper.scrape_profile(linkedin_url) - profile_data["linkedin"] = linkedin_data - logger.debug( - f"[{request.request_id}] LinkedIn profile scraped successfully" - ) - except Exception as e: - logger.warning(f"[{request.request_id}] LinkedIn scraping failed: {e}") - # Don't fail if LinkedIn scraping fails, continue with GitHub data # Step 2: Analyze job description try: diff --git a/database/mongodb.py b/database/mongodb.py index 9973e0b..848a122 100644 --- a/database/mongodb.py +++ b/database/mongodb.py @@ -123,7 +123,6 @@ class Resume(Document): user_id = StringField(required=True, unique=False, index=True) github_username = StringField() - linkedin_url = StringField() job_title = StringField() job_description = StringField() tailored_content = DictField(required=True) @@ -198,7 +197,6 @@ class User(Document): username = StringField(unique=True, required=True, index=True) password_hash = StringField(required=True) github_username = StringField() - linkedin_url = StringField() first_name = StringField() last_name = StringField() bio = StringField() diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e98ac42..c450f06 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,7 +74,7 @@ This document provides technical reference for the production implementation of 1. Client submits form with: - github_username (validated) - job_description (validated) - - linkedin_url (optional, validated) + - linkedin_data ZIP file (optional) ↓ 2. Flask endpoint receives POST /api/v1/generate-resume - Generate request_id: "a1b2c3d4" @@ -230,11 +230,6 @@ User Input │ 2. validate_job_description() │ │ - Check length (50-50000) │ │ - Raise InvalidJobDescription │ -├──────────────────────────────────┤ -│ 3. validate_linkedin_url() │ -│ - Check URL pattern │ -│ - Optional (empty allowed) │ -│ - Raise ValueError │ └──────────────────────────────────┘ ↓ Cleaned Input (or Exception raised) @@ -248,9 +243,7 @@ TestInputValidator ├── test_valid_github_username ├── test_invalid_github_username_format ├── test_valid_job_description -├── test_invalid_job_description_too_short -├── test_valid_linkedin_url -└── test_invalid_linkedin_url +└── test_invalid_job_description_too_short TestGitHubScraper ├── test_scrape_profile_success (mocked) diff --git a/docs/MONGODB_GUIDE.md b/docs/MONGODB_GUIDE.md index 3943ad2..e017b0e 100644 --- a/docs/MONGODB_GUIDE.md +++ b/docs/MONGODB_GUIDE.md @@ -76,7 +76,6 @@ user = User.objects.create( username="johndoe", password_hash="hashed...", github_username="johndoe-github", - linkedin_url="https://linkedin.com/in/johndoe", first_name="John", last_name="Doe" ) diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md index 8f5d3d1..894b3fa 100644 --- a/docs/QUICK_START.md +++ b/docs/QUICK_START.md @@ -107,7 +107,6 @@ curl -X POST http://localhost:5000/api/v1/generate-resume \ -H "Content-Type: application/json" \ -d '{ "github_username": "torvalds", - "linkedin_url": "", "job_description": "We are looking for a C programmer with 10+ years of experience working on operating systems and kernel development. Required skills: C, Assembly, Linux, Git. Nice to have: Rust, Python." }' \ -o resume.pdf diff --git a/docs/README.md b/docs/README.md index 70b120c..2f80d41 100644 --- a/docs/README.md +++ b/docs/README.md @@ -92,7 +92,7 @@ The application will start at `http://localhost:5000` 2. **Enter your profile information:** - GitHub username (e.g., `torvalds`) - - LinkedIn profile URL (optional) + - LinkedIn Data Export ZIP (optional) 3. **Paste the job description** for the position you're applying to @@ -131,11 +131,13 @@ Generates a tailored resume ```json { "github_username": "username", - "linkedin_url": "https://linkedin.com/in/username", "job_description": "Full job description text..." } ``` +**OR (with LinkedIn export)** +Multipart form with `github_username`, `job_description`, and `linkedin_data` (file) + **Response:** Returns PDF file as attachment @@ -146,7 +148,7 @@ Health check endpoint 1. **Profile Scraping**: - Extracts repositories, languages, and projects from GitHub - - (LinkedIn support is placeholder - requires API access in production) + - Parses LinkedIn data export ZIP if provided 2. **Job Analysis**: - Parses job description to extract required skills diff --git a/generator/resume_generator.py b/generator/resume_generator.py index 87b4b17..945d995 100644 --- a/generator/resume_generator.py +++ b/generator/resume_generator.py @@ -238,7 +238,6 @@ def _parse_gemini_response(self, response_text: str, profile_data: Dict) -> Dict if github_data.get("username") else "" ) - parsed["linkedin_url"] = linkedin_data.get("url", "") return parsed except Exception as e: @@ -337,7 +336,6 @@ def _generate_basic_resume( if github_data.get("username") else "" ), - "linkedin_url": linkedin_data.get("url", ""), "summary": linkedin_data.get("summary") or github_data.get("bio", "Experienced software developer"), "skills": final_skills, diff --git a/scrapers/linkedin_scraper.py b/scrapers/linkedin_scraper.py index d9a2fc4..780e3ac 100644 --- a/scrapers/linkedin_scraper.py +++ b/scrapers/linkedin_scraper.py @@ -171,52 +171,3 @@ def parse_export(self, zip_file_path: str) -> Dict: except Exception as e: print(f"Error parsing LinkedIn export: {e}") return profile_data - except Exception as e: - print(f"Error parsing LinkedIn export: {e}") - return profile_data - - def scrape_profile(self, linkedin_url: str) -> Dict: - """ - Extract LinkedIn profile data - - In this implementation, we return a placeholder structure. - In production, this would: - - Use LinkedIn's API with OAuth - - Or parse user-uploaded LinkedIn data export - - Args: - linkedin_url: LinkedIn profile URL - - Returns: - Dictionary containing profile data - """ - # Extract username from URL - username = self._extract_username(linkedin_url) - - # Log warning about placeholder usage - print( - "Warning: LinkedIn scraper is using placeholder data. " - "LinkedIn scraping requires API access or manual data export. " - "For best results, set up LinkedIn API access or ask users to provide their data." - ) - - # Return placeholder data - # In production, this would fetch real data from LinkedIn API - return { - "url": linkedin_url, - "username": username, - "name": "", - "headline": "", - "summary": "", - "experience": [], - "education": [], - "skills": [], - "note": "LinkedIn scraping requires API access or manual data export", - } - - def _extract_username(self, url: str) -> str: - """Extract username from LinkedIn URL""" - match = re.search(r"linkedin\.com/in/([^/]+)", url) - if match: - return match.group(1) - return url diff --git a/tests/test_comprehensive.py b/tests/test_comprehensive.py index aec55de..ab52079 100644 --- a/tests/test_comprehensive.py +++ b/tests/test_comprehensive.py @@ -67,35 +67,6 @@ def test_job_description_cleanup(self): result = InputValidator.validate_job_description(desc) assert result == desc.strip() - def test_valid_linkedin_url(self): - """Test valid LinkedIn URL""" - valid_urls = [ - "https://www.linkedin.com/in/username/", - "https://linkedin.com/in/username", - "http://www.linkedin.com/in/john-doe/", - ] - - for url in valid_urls: - result = InputValidator.validate_linkedin_url(url) - assert result == url - - def test_empty_linkedin_url(self): - """Test that empty LinkedIn URL is optional""" - result = InputValidator.validate_linkedin_url("") - assert result == "" - - def test_invalid_linkedin_url(self): - """Test invalid LinkedIn URL""" - invalid_urls = [ - "https://github.com/user", - "not-a-url", - "https://facebook.com/user", - ] - - for url in invalid_urls: - with pytest.raises(ValueError): - InputValidator.validate_linkedin_url(url) - def test_validate_complete_request(self): """Test complete request validation""" github = "torvalds" @@ -103,15 +74,13 @@ def test_validate_complete_request(self): "We are looking for a seasoned Python developer with 10+ years of experience " "in building scalable systems. You will work with Django, PostgreSQL, and Docker." ) - linkedin = "https://www.linkedin.com/in/test/" - result_github, result_job, result_linkedin = InputValidator.validate_request( - github, job_desc, linkedin + result_github, result_job = InputValidator.validate_request( + github, job_desc ) assert result_github == github assert result_job == job_desc - assert result_linkedin == linkedin class TestGitHubScraper: @@ -315,7 +284,6 @@ def test_generate_resume_endpoint( "/api/v1/generate-resume", json={ "github_username": "torvalds", - "linkedin_url": "", "job_description": "We need a C programmer with 10+ years " "of experience working on operating systems", }, @@ -330,7 +298,6 @@ def test_generate_resume_missing_job_description(self, client): "/api/v1/generate-resume", json={ "github_username": "torvalds", - "linkedin_url": "", "job_description": "", }, ) @@ -345,7 +312,6 @@ def test_generate_resume_missing_profiles(self, client): "/api/v1/generate-resume", json={ "github_username": "", - "linkedin_url": "", "job_description": "We need a developer with 5+ years experience", }, ) diff --git a/utils/validators.py b/utils/validators.py index d0f37e5..c7e01f6 100644 --- a/utils/validators.py +++ b/utils/validators.py @@ -22,9 +22,6 @@ class InputValidator: MIN_JOB_DESC_LENGTH = 50 MAX_JOB_DESC_LENGTH = 50000 - # LinkedIn URL pattern - LINKEDIN_URL_PATTERN = r"^https?://(?:www\.)?linkedin\.com/.*" - @classmethod def validate_github_username(cls, username: str) -> str: """ @@ -83,55 +80,28 @@ def validate_job_description(cls, job_desc: str) -> str: return job_desc - @classmethod - def validate_linkedin_url(cls, url: str) -> str: - """ - Validate LinkedIn profile URL - - Args: - url: LinkedIn URL to validate - - Returns: - Cleaned URL - - Raises: - ValueError: If URL is invalid - """ - if not url: - return "" # Optional field - - url = url.strip() - - if not re.match(cls.LINKEDIN_URL_PATTERN, url, re.IGNORECASE): - raise ValueError("Invalid LinkedIn URL format") - - return url - @classmethod def validate_request( - cls, github_username: str, job_description: str, linkedin_url: str = "" - ) -> Tuple[str, str, str]: + cls, github_username: str, job_description: str + ) -> Tuple[str, str]: """ Validate complete request input Args: github_username: GitHub username job_description: Job description - linkedin_url: LinkedIn profile URL (optional) Returns: - Tuple of (github_username, job_description, linkedin_url) + Tuple of (github_username, job_description) Raises: InvalidGitHubUsername: If username is invalid InvalidJobDescription: If job description is invalid - ValueError: If LinkedIn URL is invalid """ github_username = cls.validate_github_username(github_username) job_description = cls.validate_job_description(job_description) - linkedin_url = cls.validate_linkedin_url(linkedin_url) - return github_username, job_description, linkedin_url + return github_username, job_description @classmethod def is_valid_github_username(cls, username: str) -> bool: