From 3784b6f83e5112a41cb4d0b205ef690155fbbefe Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 22 Jan 2025 20:10:15 +0200 Subject: [PATCH] first commit --- .gitignore | 47 +++++++++++ Dockerfile | 7 ++ README.md | 41 +++++++++ envtemp | 2 + main.py | 188 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 21 +++++ utils/logging_setup.py | 37 ++++++++ 7 files changed, 343 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 envtemp create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 utils/logging_setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f38cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Игнорируем директорию с пользовательскими настройками IDE +.idea/ +.vscode/ + +# Игнорируем файлы логов +*.log + +# Игнорируем кэш и временные файлы +*.tmp +*.swp +*.bak + +# Игнорируем файлы окружения Python +*.pyc +__pycache__/ + +# Игнорируем конфигурации ОС +.DS_Store +Thumbs.db + +# Игнорируем директорию с зависимостями (например, для Node.js) +node_modules/ + +# Игнорируем файлы виртуального окружения Python +.venv/ +.env/ +test/ + +# Игнорируем скомпилированные файлы +*.out +*.class +*.o +*.log + +# Игнорируем артефакты сборки +/dist/ +/build/ +/target/ + +# Игнорируем файлы базы данных +*.sqlite +*.db + +# Исключаем файл конфигурации среды, например, переменные окружения +.env +ww +test.py \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7eef154 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4238040 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# linkedin получение данных профиля и вакансии! +# linkedin ежедневный обмен вакансий в БД + + +- [x] Профиль аккаунта по ссылке +- [ ] Вакансия по ссылке (частично) +- [ ] Ежедневный обмен вакансий (Ожидаю доступы) + + +> [!NOTE] +> Обмен по времени, один раз в сутки! +> Пишет логи! + + +> [!IMPORTANT] +> envtemp Пример .env для заполнения. + + +## Install + +``` +git clone https://git.xander.cx.ua/Xanders25/linkedin.git +``` + +> Добавляем .env (Создаём через nano в файле envtemp посмотреть какие переменные нужны ) + + + +### Работа с образами: +1. Сборка образа: + ```bash + docker build -t linkedin . + ``` + +2. Запуск контейнера: + ```bash + docker run -d --restart always --env-file .env \ + -v /home/logs:/app/logs \ + linkedin -p 8000:8000 + ``` +3. Переходим по адресу http://IP:8000 diff --git a/envtemp b/envtemp new file mode 100644 index 0000000..0b81397 --- /dev/null +++ b/envtemp @@ -0,0 +1,2 @@ +USERNAME= +PASSWD= \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b22b66e --- /dev/null +++ b/main.py @@ -0,0 +1,188 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, HttpUrl +from urllib.parse import urlparse, parse_qs +from linkedin_api import Linkedin +import logging +from utils.logging_setup import configure_global_logging +from datetime import datetime +from dotenv import load_dotenv +import os + + +# # Ваши учетные данные LinkedIn +username = os.getenv('USERNAME') +password = os.getenv('PASSWD') + +configure_global_logging() + +app = FastAPI() +api = Linkedin(username, password) + +class Profile(BaseModel): + name: str + title: str + url: HttpUrl + +class UrlModel(BaseModel): + link_profile: HttpUrl # Используем HttpUrl для проверки валидности URL + +@app.post("/link_profile") +async def profile_url(data: UrlModel): + # Проверяем, что ссылка начинается с "https://xander" + if not str(data.link_profile).startswith("https://www.linkedin.com/"): + # Возвращаем исключение с кодом 400 и сообщением об ошибке + raise HTTPException(status_code=400, detail="Неправильная ссылка") + url_profiles = str(data.link_profile) + path = urlparse(url_profiles).path + print(path) + + # Пропускаем '/in/' и получаем последнюю часть + profiles = path.split("/")[2] + + print(profiles) + + profile = api.get_profile(profiles) + logging.info(f"{profile}") + firstName = profile['firstName'] + lastName = profile['lastName'] + locationName = profile['locationName'] + geoLocationName = profile['geoLocationName'] + headline = profile['headline'] + educations = profile['education'] + education = [ + { + "schoolName": item.get("schoolName"), + "Field_of_Study": item.get("fieldOfStudy"), + "Degree": item.get('degreeName'), + "Grade": "??", + "Start Date": item.get("timePeriod", {}).get("startDate", {}).get("year"), + "End Date": item.get("timePeriod", {}).get("endDate", {}).get("year"), + "Currently study here": '??', + "Description": "??" + + + } + for item in educations + ] + + experiences = profile['experience'] + experience = [ + { + "companyName": item.get("companyName"), + "title": item.get("title"), + "Employment type": "??", + "Location": item.get('geoLocationName'), + "Location Type": "??", + "Start Date": { + "month": item.get("timePeriod", {}).get("startDate", {}).get("month"), + "year": item.get("timePeriod", {}).get("startDate", {}).get("year"), + }, + "End Date":{ + "month": item.get("timePeriod", {}).get("endDate", {}).get("month"), + "year": item.get("timePeriod", {}).get("endDate", {}).get("year"), + }, + "Currently study here": '??', + "Description": item.get('description') + + + } + for item in experiences + ] + + volunteers = profile['volunteer'] + volunteer = [ + { + "Organization": item.get("companyName"), + "Role": item.get("role"), + "Cause": item.get("cause"), + "Location": item.get('geoLocationName'), + "Start Date": { + "month": item.get("timePeriod", {}).get("startDate", {}).get("month"), + "year": item.get("timePeriod", {}).get("startDate", {}).get("year"), + }, + "End Date":{ + "month": item.get("timePeriod", {}).get("endDate", {}).get("month"), + "year": item.get("timePeriod", {}).get("endDate", {}).get("year"), + }, + "Currently study here": '??', + "Description": item.get('description') + + + } + for item in volunteers + ] + all_skills = profile['skills'] + skills = [item['name'] for item in all_skills] + all_languages = profile['languages'] + languages = [item['name'] for item in all_languages] + + + + + # Если профиль не в виде словаря, возвращаем его напрямую + return {"FirstName": firstName, + "LastName":lastName, + "Location": f"{geoLocationName} {locationName}", + "Statement":headline, + 'Education - list': education, + 'Experience - list': experience, + "Volunteering - list": volunteer, + + + + "skills": skills, + "languages":languages} + + + +@app.post("/link_vacancy") +async def vacancy_url(data: UrlModel): + if not str(data.link_profile).startswith("https://www.linkedin.com/"): + # Возвращаем исключение с кодом 400 и сообщением об ошибке + raise HTTPException(status_code=400, detail="Неправильная ссылка") + + query_params = parse_qs(urlparse(str(data.link_profile)).query) + current_job_id = query_params.get("currentJobId", [None])[0] + + jobs = api.get_job(current_job_id) + location = jobs['formattedLocation'] + title = jobs['title'] + listedAt = jobs['listedAt'] + + # Преобразование из миллисекунд в секунды и конвертация + future_date = datetime.fromtimestamp(listedAt / 1000) + + # Текущая дата + current_date = datetime.now() + + # Разница в днях + difference = abs((future_date - current_date).days) + + # print(f"Разница в днях: {difference}") + + + jobPostingId = jobs['jobPostingId'] + workplaceTypesResolutionResults = jobs['workplaceTypesResolutionResults'] + + + + # Извлекаем все localizedName + localized_names = [value['localizedName'] for value in workplaceTypesResolutionResults.values()] + + print(localized_names) + + # localized_name = workplaceTypesResolutionResults['urn:li:fs_workplaceType:2']['localizedName'] + link = f'https://www.linkedin.com/jobs/view/{current_job_id}/' + + + return {"job_id": current_job_id, + "job_title": title, + "minimum_annual_salary": f"minimum_annual_salary", + "salary_currency": 'salary_currency', + 'location_type': "location_type", + 'location': location, + "job_level": "job_level", + "job_type": localized_names, + "days_posted": difference, + "hourly_rate": "hourly_rate", + "link": link} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..80cc057 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +annotated-types==0.7.0 +anyio==4.8.0 +beautifulsoup4==4.12.3 +certifi==2024.12.14 +charset-normalizer==3.4.1 +click==8.1.8 +fastapi==0.115.6 +h11==0.14.0 +idna==3.10 +linkedin-api @ git+https://github.com/tomquirk/linkedin-api.git@dacec3c9f03b4f1fbddb4f7dfdef57ea408e40aa +lxml==5.3.0 +pydantic==2.10.5 +pydantic_core==2.27.2 +python-dotenv==1.0.1 +requests==2.32.3 +sniffio==1.3.1 +soupsieve==2.6 +starlette==0.41.3 +typing_extensions==4.12.2 +urllib3==2.3.0 +uvicorn==0.34.0 diff --git a/utils/logging_setup.py b/utils/logging_setup.py new file mode 100644 index 0000000..be2097e --- /dev/null +++ b/utils/logging_setup.py @@ -0,0 +1,37 @@ +import logging +from logging.handlers import RotatingFileHandler +from pathlib import Path + +# Глобальная настройка логирования +def configure_global_logging( + log_file="logs/app.log", + level=logging.INFO, + max_bytes=5_000_000, + backup_count=3 +): + log_file_path = Path(log_file) + log_file_path.parent.mkdir(parents=True, exist_ok=True) + + # Основной логгер + logger = logging.getLogger() + logger.setLevel(level) + + if not logger.hasHandlers(): + # Обработчик для файла + file_handler = RotatingFileHandler( + log_file, maxBytes=max_bytes, backupCount=backup_count + ) + file_handler.setLevel(level) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + # Обработчик для консоли + console_handler = logging.StreamHandler() + console_handler.setLevel(level) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + return logger + +