import os import hashlib import uuid import datetime import sqlite3 from pathlib import Path def get_sqlite_path() -> Path: base_dir = Path(__file__).resolve().parent.parent sqlite_db = base_dir / "web" / "prisma" / "dev.db" return sqlite_db def generate_job_hash(job_url: str) -> str: return hashlib.sha256(job_url.encode("utf-8")).hexdigest() def is_quality_active_job(job: dict) -> bool: """ Anti-Ghost & Quality Filter Gate: 1. Title must be substantial and not placeholder text. 2. Company cannot be generic or unknown. 3. Description must have at least 150 characters of actual role details (rejects ghost stubs). 4. URL must be a valid canonical HTTP link. 5. Rejects typical recruiter spam strings. """ title = (job.get("title") or "").strip() company = (job.get("company") or "").strip() desc = (job.get("description") or "").strip() url = (job.get("job_url") or "").strip() if len(title) < 3 or title.lower() in ["untitled", "various", "general", "n/a"]: return False if len(company) < 2 or company.lower() in ["unknown", "confidential", "confidential company", "n/a"]: return False if len(desc) < 140: return False if not url.startswith("http"): return False # Filter out obvious third-party spam aggregators / ghost posting traps low_desc = desc.lower() spam_indicators = [ "earn up to $500/day stuffing envelopes", "wire transfer assistant", "mystery shopper wanted" ] if any(s in low_desc for s in spam_indicators): return False return True def upsert_jobs(jobs_list: list): if not jobs_list: return 0 # Apply quality filter gate filtered_jobs = [j for j in jobs_list if is_quality_active_job(j)] rejected_count = len(jobs_list) - len(filtered_jobs) if rejected_count > 0: print(f"[Quality Gate] Filtered out {rejected_count} low-quality/stub/ghost postings.") db_url = os.getenv("DATABASE_URL", "") if "postgresql" in db_url: return _upsert_postgres(filtered_jobs, db_url) else: return _upsert_sqlite(filtered_jobs) def _upsert_sqlite(jobs_list: list): db_path = get_sqlite_path() if not db_path.parent.exists(): db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(db_path)) cursor = conn.cursor() query = """ INSERT INTO Job ( id, jobUrlHash, title, company, location, isRemote, department, experienceLevel, description, salaryMin, salaryMax, jobUrl, source, datePosted, createdAt, updatedAt ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (jobUrlHash) DO UPDATE SET title = excluded.title, company = excluded.company, location = excluded.location, isRemote = excluded.isRemote, department = COALESCE(excluded.department, Job.department), experienceLevel = COALESCE(excluded.experienceLevel, Job.experienceLevel), description = excluded.description, salaryMin = COALESCE(excluded.salaryMin, Job.salaryMin), salaryMax = COALESCE(excluded.salaryMax, Job.salaryMax), datePosted = COALESCE(excluded.datePosted, Job.datePosted), updatedAt = excluded.updatedAt; """ def format_iso(dt_val): if not dt_val: dt_val = datetime.datetime.now(datetime.timezone.utc) if isinstance(dt_val, str): dt_val = dt_val.strip() if len(dt_val) == 10 and "-" in dt_val: return dt_val + "T00:00:00.000Z" return dt_val if isinstance(dt_val, (datetime.datetime, datetime.date)): return dt_val.isoformat() return datetime.datetime.now(datetime.timezone.utc).isoformat() now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat() seen_hashes = set() records = [] # 1. First ensure all distinct companies exist in Company table distinct_companies = {} for j in jobs_list: c_name = (j.get("company") or "").strip() c_loc = (j.get("location") or "").strip() if c_name and len(c_name) >= 2 and c_name.lower() not in ["unknown", "confidential", "confidential company", "n/a"]: if c_name not in distinct_companies: distinct_companies[c_name] = c_loc for c_name, c_loc in distinct_companies.items(): comp_id = "comp_" + hashlib.sha256(c_name.lower().encode("utf-8")).hexdigest()[:20] cursor.execute(""" INSERT OR IGNORE INTO Company ( id, name, location, verificationStatus, trustStatus, createdAt, updatedAt ) VALUES (?, ?, ?, 'UNCLAIMED', 'UNVERIFIED', ?, ?) """, (comp_id, c_name, c_loc or "USA", now_iso, now_iso)) # Fetch mapping of company name to id cursor.execute("SELECT id, name FROM Company") comp_map = {row[1]: row[0] for row in cursor.fetchall()} for j in jobs_list: job_url = j.get("job_url", "") if not job_url: continue job_hash = generate_job_hash(job_url) if job_hash in seen_hashes: continue seen_hashes.add(job_hash) job_id = "job_" + str(uuid.uuid4()).replace("-", "")[:20] c_name = j.get("company", "Unknown Company")[:255] c_id = comp_map.get(c_name) records.append(( job_id, job_hash, j.get("title", "Untitled Position")[:255], c_name, j.get("location", "Not Specified")[:255], 1 if j.get("is_remote") else 0, j.get("department") or "Other", j.get("experience_level") or "Mid-Level", j.get("description", "") or "No description provided.", j.get("salary_min"), j.get("salary_max"), job_url, j.get("source", "jobspy"), format_iso(j.get("date_posted")), c_id, now_iso, now_iso )) query = """ INSERT INTO Job ( id, jobUrlHash, title, company, location, isRemote, department, experienceLevel, description, salaryMin, salaryMax, jobUrl, source, datePosted, companyId, createdAt, updatedAt ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (jobUrlHash) DO UPDATE SET title = excluded.title, company = excluded.company, location = excluded.location, isRemote = excluded.isRemote, department = COALESCE(excluded.department, Job.department), experienceLevel = COALESCE(excluded.experienceLevel, Job.experienceLevel), description = excluded.description, salaryMin = COALESCE(excluded.salaryMin, Job.salaryMin), salaryMax = COALESCE(excluded.salaryMax, Job.salaryMax), datePosted = COALESCE(excluded.datePosted, Job.datePosted), companyId = COALESCE(excluded.companyId, Job.companyId), updatedAt = excluded.updatedAt; """ try: cursor.executemany(query, records) # Self-healing sanitize step: reset any existing jobs falsely tagged as remote if location or title contains negative indicators cursor.execute(""" UPDATE Job SET isRemote = 0 WHERE isRemote = 1 AND ( LOWER(location) LIKE '%hybrid%' OR LOWER(location) LIKE '%onsite%' OR LOWER(location) LIKE '%on-site%' OR LOWER(location) LIKE '%not remote%' OR LOWER(location) LIKE '%in-office%' OR LOWER(location) LIKE '%in office%' OR LOWER(title) LIKE '%hybrid%' OR LOWER(title) LIKE '%onsite%' OR LOWER(title) LIKE '%on-site%' OR LOWER(title) LIKE '%not remote%' ) AND LOWER(location) NOT LIKE '%100% remote%' AND LOWER(location) NOT LIKE '%fully remote%' AND LOWER(title) NOT LIKE '%100% remote%' AND LOWER(title) NOT LIKE '%fully remote%'; """) conn.commit() count = len(records) cursor.close() conn.close() return count except Exception as e: cursor.close() conn.close() print(f"[SQLite Error] Failed to upsert: {e}") return 0 def _upsert_postgres(jobs_list: list, db_url: str): import psycopg2 from psycopg2.extras import execute_values import time if "?schema=" in db_url: db_url = db_url.split("?schema=")[0] conn = None for attempt in range(40): try: conn = psycopg2.connect(db_url) cursor = conn.cursor() cursor.execute("SELECT to_regclass('\"Job\"');") result = cursor.fetchone() table_exists = result[0] if result else None if table_exists: break cursor.close() conn.close() conn = None print(f"[Scraper] Waiting for database tables to initialize (attempt {attempt + 1}/40)...") time.sleep(5) except Exception as conn_err: print(f"[Scraper] Waiting for PostgreSQL readiness ({conn_err}) (attempt {attempt + 1}/40)...") time.sleep(5) if not conn or conn.closed: try: conn = psycopg2.connect(db_url) cursor = conn.cursor() except Exception as err: print(f"[Postgres Error] Could not connect to database: {err}") return 0 else: cursor = conn.cursor() now = datetime.datetime.now(datetime.timezone.utc) # 1. First ensure all distinct companies exist in "Company" table distinct_companies = {} for j in jobs_list: c_name = (j.get("company") or "").strip() c_loc = (j.get("location") or "").strip() if c_name and len(c_name) >= 2 and c_name.lower() not in ["unknown", "confidential", "confidential company", "n/a"]: if c_name not in distinct_companies: distinct_companies[c_name] = c_loc if distinct_companies: company_records = [] for c_name, c_loc in distinct_companies.items(): comp_id = "comp_" + hashlib.sha256(c_name.lower().encode("utf-8")).hexdigest()[:20] company_records.append(( comp_id, c_name, c_loc or "USA", "UNCLAIMED", "UNVERIFIED", now, now )) comp_upsert_query = """ INSERT INTO "Company" ("id", "name", "location", "verificationStatus", "trustStatus", "createdAt", "updatedAt") VALUES %s ON CONFLICT ("name") DO UPDATE SET "location" = COALESCE("Company"."location", EXCLUDED."location"), "updatedAt" = EXCLUDED."updatedAt"; """ try: execute_values(cursor, comp_upsert_query, company_records) conn.commit() except Exception as comp_err: conn.rollback() print(f"[Postgres Warning] Company pre-population error: {comp_err}") # Fetch mapping of company name to id comp_map = {} try: cursor.execute('SELECT "id", "name" FROM "Company"') for row in cursor.fetchall(): comp_map[row[1]] = row[0] except Exception as map_err: print(f"[Postgres Warning] Failed to fetch company map: {map_err}") query = """ INSERT INTO "Job" ( "id", "jobUrlHash", "title", "company", "location", "isRemote", "department", "experienceLevel", "description", "salaryMin", "salaryMax", "jobUrl", "source", "datePosted", "companyId", "lifecycleStatus", "lastSeenAt", "createdAt", "updatedAt" ) VALUES %s ON CONFLICT ("jobUrlHash") DO UPDATE SET "title" = EXCLUDED."title", "company" = EXCLUDED."company", "location" = EXCLUDED."location", "isRemote" = EXCLUDED."isRemote", "department" = COALESCE(EXCLUDED."department", "Job"."department"), "experienceLevel" = COALESCE(EXCLUDED."experienceLevel", "Job"."experienceLevel"), "description" = EXCLUDED."description", "salaryMin" = COALESCE(EXCLUDED."salaryMin", "Job"."salaryMin"), "salaryMax" = COALESCE(EXCLUDED."salaryMax", "Job"."salaryMax"), "datePosted" = COALESCE(EXCLUDED."datePosted", "Job"."datePosted"), "companyId" = COALESCE(EXCLUDED."companyId", "Job"."companyId"), "lifecycleStatus" = 'ACTIVE', "lastSeenAt" = NOW(), "updatedAt" = NOW(); """ records = [] seen_hashes = set() for j in jobs_list: job_url = j.get("job_url", "") if not job_url: continue job_hash = generate_job_hash(job_url) if job_hash in seen_hashes: continue seen_hashes.add(job_hash) job_id = "job_" + str(uuid.uuid4()).replace("-", "")[:20] c_name = j.get("company", "Unknown Company")[:255] c_id = comp_map.get(c_name) records.append(( job_id, job_hash, j.get("title", "Untitled Position")[:255], c_name, j.get("location", "Not Specified")[:255], bool(j.get("is_remote", False)), j.get("department") or "Other", j.get("experience_level") or "Mid-Level", j.get("description", "") or "No description provided.", j.get("salary_min"), j.get("salary_max"), job_url, j.get("source", "jobspy"), now, c_id, "ACTIVE", now, now, now )) try: execute_values(cursor, query, records) # Self-healing sanitize step: reset any existing jobs falsely tagged as remote if location or title contains negative indicators cursor.execute(""" UPDATE "Job" SET "isRemote" = FALSE WHERE "isRemote" = TRUE AND ( LOWER("location") LIKE '%hybrid%' OR LOWER("location") LIKE '%onsite%' OR LOWER("location") LIKE '%on-site%' OR LOWER("location") LIKE '%not remote%' OR LOWER("location") LIKE '%in-office%' OR LOWER("location") LIKE '%in office%' OR LOWER("title") LIKE '%hybrid%' OR LOWER("title") LIKE '%onsite%' OR LOWER("title") LIKE '%on-site%' OR LOWER("title") LIKE '%not remote%' ) AND LOWER("location") NOT LIKE '%100% remote%' AND LOWER("location") NOT LIKE '%fully remote%' AND LOWER("title") NOT LIKE '%100% remote%' AND LOWER("title") NOT LIKE '%fully remote%'; """) conn.commit() count = len(records) cursor.close() conn.close() return count except Exception as e: conn.rollback() cursor.close() conn.close() print(f"[Postgres Error] Failed to upsert: {e}") return 0