Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,7 @@
</div>

<div class="form-group">
<label for="linkedin">LinkedIn Profile URL (optional):</label>
<input type="text" id="linkedin" name="linkedin" placeholder="https://www.linkedin.com/in/username/">
<div class="example">Example: https://www.linkedin.com/in/satyanadella/</div>
</div>

<div class="form-group">
<label for="linkedinFile">OR Upload LinkedIn Data (ZIP):</label>
<label for="linkedinFile">Upload LinkedIn Data (ZIP):</label>
<input type="file" id="linkedinFile" name="linkedinFile" accept=".zip">
<div class="example">Settings > Data Privacy > Get a copy of your data > Download ZIP</div>
</div>
Expand All @@ -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;
}

Expand All @@ -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);

Expand All @@ -216,7 +208,6 @@
},
body: JSON.stringify({
github_username: github,
linkedin_url: linkedin,
job_description: jobDescription
})
});
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 7 additions & 31 deletions app_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,7 @@
</div>

<div class="form-group">
<label for="linkedin">LinkedIn Profile URL (optional):</label>
<input type="text" id="linkedin" name="linkedin" placeholder="https://www.linkedin.com/in/username/">
<div class="example">Example: https://www.linkedin.com/in/satyanadella/</div>
</div>

<div class="form-group">
<label for="linkedinFile">OR Upload LinkedIn Data (ZIP):</label>
<label for="linkedinFile">Upload LinkedIn Data (ZIP):</label>
<input type="file" id="linkedinFile" name="linkedinFile" accept=".zip">
<div class="example">Settings > Data Privacy > Get a copy of your data > Download ZIP</div>
</div>
Expand All @@ -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;
}

Expand All @@ -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);

Expand All @@ -266,7 +258,6 @@
},
body: JSON.stringify({
github_username: github,
linkedin_url: linkedin,
job_description: jobDescription
})
});
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 0 additions & 2 deletions database/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
11 changes: 2 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion docs/MONGODB_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
1 change: 0 additions & 1 deletion docs/QUICK_START.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions generator/resume_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
49 changes: 0 additions & 49 deletions scrapers/linkedin_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading