first commit

This commit is contained in:
Xander 2025-01-22 20:10:15 +02:00
commit 3784b6f83e
7 changed files with 343 additions and 0 deletions

47
.gitignore vendored Normal file
View File

@ -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

7
Dockerfile Normal file
View File

@ -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"]

41
README.md Normal file
View File

@ -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

2
envtemp Normal file
View File

@ -0,0 +1,2 @@
USERNAME=
PASSWD=

188
main.py Normal file
View File

@ -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}

21
requirements.txt Normal file
View File

@ -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

37
utils/logging_setup.py Normal file
View File

@ -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