From 4bc0d9f685831799d9a4d7ed32174d1348b761ed Mon Sep 17 00:00:00 2001 From: Senior YAML Developer Date: Mon, 6 Mar 2023 09:01:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B5=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=80=D0=B0=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=D1=8F?= =?UTF-8?q?=D1=89=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .vscode/launch.json | 24 ++ README.md | 134 +++++++ hh_applicant_tool/__init__.py | 0 hh_applicant_tool/__main__.py | 6 + hh_applicant_tool/api.py | 219 +++++++++++ hh_applicant_tool/color_log.py | 35 ++ hh_applicant_tool/contsants.py | 8 + hh_applicant_tool/main.py | 103 ++++++ hh_applicant_tool/operations/__init__.py | 0 hh_applicant_tool/operations/add_handler.py | 31 ++ hh_applicant_tool/operations/apply_jobs.py | 95 +++++ hh_applicant_tool/operations/authorize.py | 67 ++++ hh_applicant_tool/operations/whoami.py | 29 ++ hh_applicant_tool/types.py | 37 ++ hh_applicant_tool/utils.py | 43 +++ poetry.lock | 379 ++++++++++++++++++++ pyproject.toml | 25 ++ tests/__init__.py | 0 19 files changed, 1237 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 hh_applicant_tool/__init__.py create mode 100644 hh_applicant_tool/__main__.py create mode 100644 hh_applicant_tool/api.py create mode 100644 hh_applicant_tool/color_log.py create mode 100644 hh_applicant_tool/contsants.py create mode 100644 hh_applicant_tool/main.py create mode 100644 hh_applicant_tool/operations/__init__.py create mode 100644 hh_applicant_tool/operations/add_handler.py create mode 100644 hh_applicant_tool/operations/apply_jobs.py create mode 100644 hh_applicant_tool/operations/authorize.py create mode 100644 hh_applicant_tool/operations/whoami.py create mode 100644 hh_applicant_tool/types.py create mode 100644 hh_applicant_tool/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9195364 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +/.venv diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f90d6f7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Authorize", + "type": "python", + "request": "launch", + "module": "hh_applicant_tool", + "args": ["-vv", "authorize"], + "justMyCode": true + }, + { + "name": "Apply Jobs", + "type": "python", + "request": "launch", + "module": "hh_applicant_tool", + "args": ["-vv", "apply-jobs"], + "justMyCode": true + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e078cc2 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# HH Applicant Tool + +Утилита для автоматизации таких действий на HH как автоматический отклик на подходящие вакансии. + +Системные требования: + +- socat +- python >= 3.10 + +Нужную версию можно поставить через asdf/pyenv, а вот socat придется доставить. + +Данная утилита не может работать от root. Так что любители кало-линукса идут нахуй. Туда же идут пользователи Windows, так как я им не пользуюсь, но никто вам не мешает его добавить. + +Предыстория. + +В общем у меня был один знакомый знакомого, который работал ХЕРом. Этот чувак не заморачивался с чтением резюме, а тупо скриптами рассылал предложения о работе... Бывают, конечно, филологини, которые не могут отлчитьб Java от JavaScript, но я думаю, что в значительном числе ситуаций тдет именно о рассылках... И я просто перенял эту тактику. Долгое время я делал массовые заявки с помощью консоли браузера: + +```js +$$('[data-qa="vacancy-serp__vacancy_response"]').forEach((el) => el.click()); +``` + +И оно работает, хоть и не идеально. Я даже пробовал автоматизировать рассылки через `p[yu]ppeeter`, пока не прочитал [документацию](https://github.com/hhru/api). И не обнаружил много там интересных методов, использование которых через кукловода проблематично... + +Данное приложение работает через CLIENT_ID и CLIENT_SECRET, полученные мною путем [декомпиляции официального приложения для Android](https://gist.github.com/s3rgeym/eee96bbf91b04f7eb46b7449f8884a00). Не знаю считается это взломом или нет. + +Установка: + +```bash +# Через pypi +# Можно использовать и обычный pip +$ pipx install hh-applicant-tool + +# Если хочется использовать самую последнюю версию, то можно установить ее через git +$ pipx install git+https://github.com/s3rgeym/hh-applicant-tool +``` + +Помощь: + +```bash +$ hh-applicant-tool +... + +$ hh-applicant-tool apply-jobs -h +usage: hh-applicant-tool apply-jobs [-h] [--resume-id RESUME_ID] [--message-list MESSAGE_LIST] + +Откликнуться на все подходящие вакансии + +options: + -h, --help show this help message and exit + --resume-id RESUME_ID + Идентефикатор резюме + --message-list MESSAGE_LIST + Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки. В сообщения можно использовать плейсхолдеры типа + %(name)s +``` + +Для начала нужно добавить обработчик протокола `hhandroid`, который используется Android-приложением для усложнения жизни честным автоматизаторам: + +```bash +$ hh-applicant-tool -vv add-handler +[I] saved /home/sergey/.local/share/applications/hhandroid.desktop +✅ Обработчик добавлен! +``` + +Флаг `-v` используется для вывода отладочной информации. Два таких флага выводят всю возможную. + +Авторизуемся: + +```bash +$ hh_applicant_tool -vv authorize +Пробуем открыть в браузере: https://hh.ru/oauth/authorize?client_id=HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD&response_type=code +Авторизуйтесь и нажмите <<Подтвердить>> +[I] 🚀 Стартуем TCP-сервер по адресу unix:///tmp/hhandroid.sock +Gtk-Message: 20:52:59.280: Failed to load module "canberra-gtk-module" +Gtk-Message: 20:52:59.975: Failed to load module "canberra-gtk-module" +[54:54:0305/205300.038812:ERROR:gl_factory.cc(128)] Requested GL implementation (gl=desktop-gl,angle=none) not found in allowed implementations: [(gl=egl-angle,angle=default),(gl=egl-gles2,angle=none),(gl=egl-angle,angle=swiftshader)]. +[54:54:0305/205300.041723:ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization +Opening in existing browser session. +[D] hhandroid://oauthresponse?code=99Q9G1RII75D8R2FTU06BF2FDNI7JF16MGBIB4OEQ973819OOJI90S69I1CL9U96 +[D] POST https://hh.ru/oauth/token 200 +[D] Сохраняем токен +🔓 Авторизация прошла успешно! +``` + +![image](https://user-images.githubusercontent.com/12753171/222978533-ed30a918-ed15-4a81-a8c2-f083e8469c16.png) + +Тут надо выбирать `Open xdg-open`. + +После смотрим в консоль и закрываем вкладку. Она сама не закроется. + +При авторизации можно указать `redirect_uri`, но любые адреса кроме того, что с протоколом `hhandroid`, будут приводить к ошибке: + +![](https://user-images.githubusercontent.com/12753171/222870516-b29f2417-d11a-4122-8291-7d440a422a31.png) + +Поэтому и нужно добавление обработчика кастомного протокола. На том шаге создается `desktop` файл, где в секции `Exec` всего пару команд для того чтобы записать полученный uri в сокет. TCP-сервер, который запускается при авторизации, как раз слушает этот сокет... Идея была хорошей для защиты от скрипт-кидис, но Сему ведь этим не остановить (c). + +Проверка: + +```bash +$ hh-applicant-tool whoami +{ + "auth_type": "applicant", + "counters": { + "new_resume_views": 1488, + "resumes_count": 1, + "unread_negotiations": 228 + }, + "email": "vasya.pupkin@gmail.com", + "employer": null, + "first_name": "Вася", + "id": "1234567890", + "is_admin": false, + "is_anonymous": false, + "is_applicant": true, + "is_application": false, + "is_employer": false, + "is_in_search": true, + "last_name": "Пупкин", + "manager": null, + "mid_name": null, + "middle_name": null, + "negotiations_url": "https://api.hh.ru/negotiations", + "personal_manager": null, + "phone": "79012345678", + "profile_videos": { + "items": [] + }, + "resumes_url": "https://api.hh.ru/resumes/mine" +} +``` + +Утилита использует систему плагинов. Все они лежат в `hh_applicant_tool/operations`. Модули расположенные там автоматически добавляются как доступные операции. За основу для своего плагина можно взять `whoami.py`. + +Отдельные замечания у меня к API. Оно какое-то кривое. Какой долбоеб придумал при создании объекта отдавать пустой ответ (по REST должен быть созданный объект) либо вообще перенаправлять на полную версию сайта? Так же в ответах сервера нет Content-Length. Я так понял там какой-то прокси оборачивает все запросы и отдает всегда TE Chunked. А еще он возвращает 502 ошибку, когда бекенд на Java падает (я почти уверен, что HH написан на ней). А [язык запросов](https://hh.ru/article/1175) мне понравился. Можно что-то типа этого использовать `NOT (!ID:123 OR !ID:456 OR !ID:789)` что бы отсеить какие-то вакансии. diff --git a/hh_applicant_tool/__init__.py b/hh_applicant_tool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hh_applicant_tool/__main__.py b/hh_applicant_tool/__main__.py new file mode 100644 index 0000000..21eba35 --- /dev/null +++ b/hh_applicant_tool/__main__.py @@ -0,0 +1,6 @@ +import sys + +from .main import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hh_applicant_tool/api.py b/hh_applicant_tool/api.py new file mode 100644 index 0000000..41df0c9 --- /dev/null +++ b/hh_applicant_tool/api.py @@ -0,0 +1,219 @@ +"""https://api.hh.ru/openapi/redoc""" +from __future__ import annotations + +import dataclasses +import json +import logging +import time +from copy import deepcopy +from dataclasses import dataclass +from functools import partialmethod +from threading import Lock +from typing import Any, Literal, TypedDict +from urllib.parse import urlencode + +import requests +from requests import Response, Session + +from .contsants import HHANDROID_CLIENT_ID, HHANDROID_CLIENT_SECRET + +logger = logging.getLogger(__package__) + + +class BaseException(Exception): + def __init__(self, data: dict[str, Any]) -> None: + self._raw = deepcopy(data) + + def __getattr__(self, name: str) -> Any: + try: + return self._raw[name] + except KeyError as ex: + raise AttributeError(name) from ex + + def __str__(self) -> str: + return str(self._raw) + + +class BadRequest(BaseException): + @property + def limit_exceeded(self) -> bool: + return any(x["value"] == "limit_exceeded" for x in self.errors) + + +class Forbidden(BaseException): + pass + + +class ResourceNotFound(BaseException): + pass + + +# По всей видимости, прокси возвращает, когда их бекенд на Java падает +# {'description': 'Bad Gateway', 'errors': [{'type': 'bad_gateway'}], 'request_id': ''} +class BadGateaway(BaseException): + pass + + +# Thread-safe +@dataclass +class BaseClient: + _: dataclasses.KW_ONLY + base_url: str + request_body_json: bool = False + # TODO: сделать генерацию User-Agent'а как в приложении + user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" + session: Session | None = None + previous_request_time: float = 0.0 + + def __post_init__(self) -> None: + self.lock = Lock() + if not self.session: + self.session = session = requests.session() + session.headers.update( + { + **self.additional_headers(), + "User-Agent": self.user_agent, + } + ) + + def additional_headers(self) -> dict[str, str]: + return {} + + def request( + self, + method: Literal["GET", "POST", "PUT", "DELETE"], + endpoint: str, + params: dict | None = None, + delay: float = 0.3, + **kwargs: Any, + ) -> dict: + assert method == method.upper() + params = dict(params or {}) + params.update(kwargs) + url = self.resolve_url(endpoint) + with self.lock: + # На серваке какая-то анти-DDOS система + if (delay := delay - time.monotonic() + self.previous_request_time) > 0: + logger.debug("wait %fs", delay) + time.sleep(delay) + response = self.session.request( + method, + url, + **{["data", "json"][self.request_body_json]: params} + if method in ["POST", "PUT"] + else dict(params=params), + allow_redirects=False, + ) + try: + # У этих лошков сервер не отдает Content-Length, а кривое API отдает пустые ответы, например, при отклике на вакансии + # 'Server': 'ddos-guard' + # ... + # 'Transfer-Encoding': 'chunked' + try: + data = response.json() + except json.decoder.JSONDecodeError: + if response.status_code in [201, 204]: + data = {} + else: + raise + finally: + logger.debug( + "%s %.88s %d", + method, + url + ("?" + urlencode(params) if params else ""), + response.status_code, + ) + self.previous_request_time = time.monotonic() + self.raise_for_status(response, data) + return data + + get = partialmethod(request, "GET") + post = partialmethod(request, "POST") + put = partialmethod(request, "PUT") + delete = partialmethod(request, "DELETE") + + def resolve_url(self, url: str) -> str: + return url if "://" in url else f"{self.base_url.rstrip('/')}/{url.lstrip('/')}" + + @staticmethod + def raise_for_status(response: Response, data: dict) -> None: + match response.status_code: + case 400: + raise BadRequest(data) + case 403: + raise Forbidden(data) + case 404: + raise ResourceNotFound(data) + case 502: + raise BadGateaway(data) + + +class AccessToken(TypedDict): + access_token: str + refresh_token: str + expires_in: int + token_type: Literal["bearer"] + + +@dataclass +class OAuthClient(BaseClient): + _: dataclasses.KW_ONLY + base_url: str = "https://hh.ru/oauth" + client_id: str = HHANDROID_CLIENT_ID + client_secret: str = HHANDROID_CLIENT_SECRET + state: str = "" + scope: str = "" + redirect_uri: str = "" + + @property + def authorize_url(self) -> str: + params = dict( + client_id=self.client_id, + redirect_uri=self.redirect_uri, + response_type="code", + scope=self.scope, + state=self.state, + ) + params_qs = urlencode({k: v for k, v in params.items() if v}) + return self.resolve_url(f"/authorize?{params_qs}") + + def authenticate(self, code: str) -> AccessToken: + params = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "code": code, + "grant_type": "authorization_code", + } + return self.request("POST", "/token", params) + + def refresh_access(self, refresh_token: str) -> AccessToken: + # refresh_token можно использовать только один раз и только по истечению срока действия access_token. + return self.request( + "POST", + "/token", + {"grant_type": "refresh_token", "refresh_token": refresh_token}, + ) + + +@dataclass +class ApiClient(BaseClient): + _: dataclasses.KW_ONLY + access_token: str | None = None + refresh_token: str | None = None + base_url: str = "https://api.hh.ru/" + # request_body_json: bool = True + oauth_client: OAuthClient | None = None + + def __post_init__(self) -> None: + super().__post_init__() + self.oauth_client = self.oauth_client or OAuthClient(session=self.session) + + def additional_headers(self) -> dict[str, str]: + return {"Authorization": f"Bearer {self.access_token}"} + + def refresh_access(self) -> None: + tok = self.oauth_client.refresh_access(self.refresh_token) + self.access_token, self.refresh_access = ( + tok["access_token"], + tok["refresh_token"], + ) diff --git a/hh_applicant_tool/color_log.py b/hh_applicant_tool/color_log.py new file mode 100644 index 0000000..92afcd8 --- /dev/null +++ b/hh_applicant_tool/color_log.py @@ -0,0 +1,35 @@ +import enum +import logging +from enum import auto + + +class Color(enum.Enum): + BLACK = 30 + RED = auto() + GREEN = auto() + YELLOW = auto() + BLUE = auto() + PURPLE = auto() + CYAN = auto() + WHITE = auto() + + def __str__(self) -> str: + return str(self.value) + + +class ColorHandler(logging.StreamHandler): + _color_map = { + "CRITICAL": Color.RED, + "ERROR": Color.RED, + "WARNING": Color.RED, + "INFO": Color.GREEN, + "DEBUG": Color.BLUE, + } + + def format(self, record: logging.LogRecord) -> str: + message = super().format(record) + isatty = getattr(self.stream, "isatty", None) + if isatty and isatty(): + color_code = self._color_map[record.levelname] + return f"\033[{color_code}m{message}\033[0m" + return message diff --git a/hh_applicant_tool/contsants.py b/hh_applicant_tool/contsants.py new file mode 100644 index 0000000..c0649ae --- /dev/null +++ b/hh_applicant_tool/contsants.py @@ -0,0 +1,8 @@ +HHANDROID_CLIENT_ID = "HIOMIAS39CA9DICTA7JIO64LQKQJF5AGIK74G9ITJKLNEDAOH5FHS5G1JI7FOEGD" +HHANDROID_CLIENT_SECRET = ( + "V9M870DE342BGHFRUJ5FTCGCUA1482AN0DI8C5TFI9ULMA89H10N60NOP8I4JMVS" +) +HHANDROID_SOCKET_PATH = "/tmp/hhandroid.sock" + +# Кривой формат, который используют эти долбоебы +INVALID_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S%z" diff --git a/hh_applicant_tool/main.py b/hh_applicant_tool/main.py new file mode 100644 index 0000000..b46f02d --- /dev/null +++ b/hh_applicant_tool/main.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import argparse +import dataclasses +import logging +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from importlib import import_module +from os import getenv +from pathlib import Path +from pkgutil import iter_modules +from typing import Sequence + +from .color_log import ColorHandler +from .utils import Config + +DEFAULT_CONFIG_PATH = ( + Path(getenv("XDG_CONFIG_PATH", Path.home() / ".config")) + / __package__.replace("_", "-") + / "config.json" +) + +logger = logging.getLogger(__package__) + + +class BaseOperation(metaclass=ABCMeta): + def add_parser_arguments(self, parser: argparse.ArgumentParser) -> None: + ... + + @abstractmethod + def run(self, args: argparse.Namespace) -> None | int: + ... + + +OPERATIONS = "operations" + + +class Namespace(argparse.Namespace): + config: Config + verbosity: int + + +@dataclass +class HHApplicantTool: + """Утилита для автоматизации действий соискателя на сайте hh.ru. + Описание, исходники и предложения: . + """ + + _: dataclasses.KW_ONLY + + def parse_args( + self, argv: Sequence[str] | None + ) -> tuple[argparse.ArgumentParser, Namespace]: + parser = argparse.ArgumentParser( + description=self.__doc__, + ) + parser.add_argument( + "-c", + "--config", + help="config path", + type=Config, + default=Config(DEFAULT_CONFIG_PATH), + ) + parser.add_argument( + "-v", + "--verbosity", + help="increase verbosity", + action="count", + default=0, + ) + subparsers = parser.add_subparsers(help="commands") + package_dir = Path(__file__).resolve().parent / OPERATIONS + for _, module_name, _ in iter_modules([str(package_dir)]): + mod = import_module(f"{__package__}.{OPERATIONS}.{module_name}") + op: BaseOperation = mod.Operation() + op_parser = subparsers.add_parser( + module_name.replace("_", "-"), description=op.__doc__ + ) + op_parser.set_defaults(run=op.run) + op.add_parser_arguments(op_parser) + parser.set_defaults(run=None) + return parser, parser.parse_args(argv) + + def run(self, argv: Sequence[str] | None) -> None | int: + parser, args = self.parse_args(argv) + log_level = max(logging.DEBUG, logging.WARNING - args.verbosity * 10) + logger.setLevel(log_level) + handler = ColorHandler() + # [C] Critical Error Occurred + handler.setFormatter(logging.Formatter("[%(levelname).1s] %(message)s")) + logger.addHandler(handler) + if args.run: + try: + return args.run(args) + except Exception as e: + logger.exception(e) + return 1 + parser.print_help() + return 2 + + +def main(argv: Sequence[str] | None = None) -> None | int: + return HHApplicantTool().run(argv) diff --git a/hh_applicant_tool/operations/__init__.py b/hh_applicant_tool/operations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hh_applicant_tool/operations/add_handler.py b/hh_applicant_tool/operations/add_handler.py new file mode 100644 index 0000000..7a285c6 --- /dev/null +++ b/hh_applicant_tool/operations/add_handler.py @@ -0,0 +1,31 @@ +import argparse +from pathlib import Path +from subprocess import check_call + +from ..contsants import HHANDROID_SOCKET_PATH +from ..main import BaseOperation, logger + +DESKTOP_ENTRY = f"""[Desktop Entry] +Name=hhandroid protocol handler +Exec=sh -c 'printf %u | socat UNIX-CONNECT:{HHANDROID_SOCKET_PATH} -' +Type=Application +Terminal=false +MimeType=x-scheme-handler/hhandroid +""" + + +class Operation(BaseOperation): + """Добавляет обработчик для протокола hhandroid, используемого Android-приложением при авторизации""" + + def add_parser_arguments(self, parser: argparse.ArgumentParser) -> None: + pass + + def run(self, args: argparse.Namespace) -> None: + # TODO: с root не будет работать + desktop_path = Path( + "~/.local/share/applications/hhandroid.desktop" + ).expanduser() + desktop_path.write_text(DESKTOP_ENTRY) + logger.info("saved %s", desktop_path) + check_call(["update-desktop-database", str(desktop_path.parent)]) + print("✅ Обработчик добавлен!") diff --git a/hh_applicant_tool/operations/apply_jobs.py b/hh_applicant_tool/operations/apply_jobs.py new file mode 100644 index 0000000..a2cf1bf --- /dev/null +++ b/hh_applicant_tool/operations/apply_jobs.py @@ -0,0 +1,95 @@ +# Этот модуль можно использовать как образец для других +import argparse +import logging +import random +from typing import TextIO + +from ..api import ApiClient, BadGateaway, BadRequest +from ..contsants import INVALID_ISO8601_FORMAT +from ..main import BaseOperation +from ..main import Namespace as BaseNamespace +from ..types import ApiListResponse, VacancyItem + +logger = logging.getLogger(__package__) + + +class Namespace(BaseNamespace): + resume_id: str | None + message_list: TextIO + + +class Operation(BaseOperation): + """Откликнуться на все подходящие вакансии""" + + def add_parser_arguments(self, parser: argparse.ArgumentParser) -> None: + parser.add_argument("--resume-id", help="Идентефикатор резюме") + parser.add_argument( + "--message-list", + help="Путь до файла, где хранятся сообщения для отклика на вакансии. Каждое сообщение — с новой строки. В сообщения можно использовать плейсхолдеры типа %%(name)s", + type=argparse.FileType(), + ) + + def run(self, args: Namespace) -> None: + assert args.config["access_token"] + if args.message_list: + application_messages = list(filter(None, map(str.strip, args.message_list))) + else: + application_messages = [ + "Меня заинтересовала Ваша вакансия %(name)s", + "Прошу рассмотреть мою кандидатуру на вакансию %(name)s", + ] + api = ApiClient( + access_token=args.config["access_token"], + ) + if not (resume_id := args.resume_id): + resumes: ApiListResponse = api.get("/resumes/mine") + # Используем id первого резюме + # TODO: создать 10 резюме и рассылать по 2000 откликов в сутки + resume_id = resumes["items"][0]["id"] + self._apply_jobs(api, resume_id, application_messages) + print("📝 Отклики на вакансии разосланы!") + + def _apply_jobs( + self, api: ApiClient, resume_id: str, application_messages: list[str] + ) -> None: + # Получаем список рекомендованных вакансий и отправляем заявки + # Проблема тут в том, что вакансии на которые мы отклимкались должны исчезать из поиска, но ОНИ ТАМ ПРИСУТСТВУЮТ. Так же есть вакансии с ебучими тестами, которые всегда вверху. + + # Я пробовал сортировать по дате, НО date_from обраьатывается правильно, а если в date_to подставить значение published_at, то все свалится, ПОТОМУ ЧТО НЕПРАВИЛЬНЫЙ ФОРМАТ. ПИДОРЫ ВЫ КРИВОРУКИЕ! + + # Там на сервере НЕ МОСКОВСКОЕ ВРЕМЯ, а какое-то свое пидорское + # date_to = datetime.strftime(datetime.now(), INVALID_ISO8601_FORMAT) + date_max = "" + while True: + vacancies: ApiListResponse = api.get( + f"/resumes/{resume_id}/similar_vacancies", + per_page=100, + order_by="publication_time", + ) + item: VacancyItem + for item in vacancies["items"]: + # В рот я ебал вас и ваши тесты, пидоры + if item["has_test"]: + continue + # Откликаемся на ваканчию + params = { + "resume_id": resume_id, + "vacancy_id": item["id"], + "message": random.choice(application_messages) % item + if item["response_letter_required"] + else "", + } + try: + # res = api.post("/negotiations", params) + # assert res == {} + logger.debug( + "Отправлен отклик на вакансию #%s %s", item["id"], item["name"] + ) + except (BadGateaway, BadRequest) as ex: + logger.warning(ex) + if isinstance(ex, BadRequest) and ex.limit_exceeded: + return + if vacancies["pages"] == 1: + break + # published = datetime.strptime(item["published_at"], INVALID_ISO8601_FORMAT) + date_max = item["published_at"] diff --git a/hh_applicant_tool/operations/authorize.py b/hh_applicant_tool/operations/authorize.py new file mode 100644 index 0000000..c68e23f --- /dev/null +++ b/hh_applicant_tool/operations/authorize.py @@ -0,0 +1,67 @@ +import argparse +import logging +import socketserver +import subprocess +from pathlib import Path +from urllib.parse import parse_qs, urlsplit + +from ..api import OAuthClient +from ..contsants import HHANDROID_SOCKET_PATH +from ..main import BaseOperation, Namespace +from ..utils import Config + +logger = logging.getLogger(__package__) + + +class HHAndroidProtocolServer(socketserver.ThreadingUnixStreamServer): + def __init__( + self, socket_path: Path | str, oauth_client: OAuthClient, config: Config + ) -> None: + self._socket_path = Path(socket_path) + self._oauth_client = oauth_client + self._config = config + super().__init__(str(self._socket_path), HHAndroidProtocolHandler) + + def server_bind(self) -> None: + self._socket_path.parent.mkdir(parents=True, exist_ok=True) + self._socket_path.unlink(missing_ok=True) + return super().server_bind() + + def server_close(self) -> None: + self._socket_path.unlink() + return super().server_close() + + def handle_redirect_uri(self, redirect_uri: str) -> None: + logger.debug(redirect_uri) + sp = urlsplit(redirect_uri) + assert sp.scheme == "hhandroid" + assert sp.netloc == "oauthresponse" + code = parse_qs(sp.query)["code"][0] + token = self._oauth_client.authenticate(code) + logger.debug("Сохраняем токен") + self._config.save(token) + self.shutdown() + + +class HHAndroidProtocolHandler(socketserver.BaseRequestHandler): + def handle(self) -> None: + self.server.handle_redirect_uri(self.request.recv(1024).decode()) + + +class Operation(BaseOperation): + """Авторизоваться на сайте""" + + def add_parser_arguments(self, parser: argparse.ArgumentParser) -> None: + pass + + def run(self, args: Namespace) -> None: + oauth = OAuthClient() + print("Пробуем открыть в браузере:", oauth.authorize_url) + subprocess.Popen(["xdg-open", oauth.authorize_url]) + print("Авторизуйтесь и нажмите <<Подтвердить>>") + logger.info("🚀 Стартуем TCP-сервер по адресу unix://%s", HHANDROID_SOCKET_PATH) + server = HHAndroidProtocolServer( + HHANDROID_SOCKET_PATH, oauth_client=oauth, config=args.config + ) + server.serve_forever() + print("🔓 Авторизация прошла успешно!") diff --git a/hh_applicant_tool/operations/whoami.py b/hh_applicant_tool/operations/whoami.py new file mode 100644 index 0000000..a0a94c3 --- /dev/null +++ b/hh_applicant_tool/operations/whoami.py @@ -0,0 +1,29 @@ +# Этот модуль можно использовать как образец для других +import argparse +import logging + +from ..api import ApiClient +from ..main import BaseOperation +from ..main import Namespace as BaseNamespace +from ..utils import dumps + +logger = logging.getLogger(__package__) + + +class Namespace(BaseNamespace): + pass + + +class Operation(BaseOperation): + """Выведет текущего пользователя""" + + def add_parser_arguments(self, parser: argparse.ArgumentParser) -> None: + pass + + def run(self, args: Namespace) -> None: + assert args.config["access_token"] + api = ApiClient( + access_token=args.config["access_token"], + ) + result = api.get("/me") + print(dumps(result)) diff --git a/hh_applicant_tool/types.py b/hh_applicant_tool/types.py new file mode 100644 index 0000000..453efe6 --- /dev/null +++ b/hh_applicant_tool/types.py @@ -0,0 +1,37 @@ +from typing import TypedDict + + +class ApiListResponse(TypedDict): + ... + items: list + found: int + page: int + pages: int + per_page: int + + +class VacancyItem(TypedDict): + accept_incomplete_resumes: bool + address: dict + alternate_url: str + apply_alternate_url: str + area: dict + contacts: dict + counters: dict + department: dict + employer: dict + has_test: bool + id: int + insider_interview: dict + name: str + professional_roles: list + published_at: str + relations: list + response_letter_required: bool + response_url: str | None + salary: dict + schedule: dict + snippet: dict + sort_point_distance: float + type: dict + url: str diff --git a/hh_applicant_tool/utils.py b/hh_applicant_tool/utils.py new file mode 100644 index 0000000..ed28ba6 --- /dev/null +++ b/hh_applicant_tool/utils.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import json +from functools import partial +from pathlib import Path +from threading import Lock +from typing import Any + +json_dump_kwargs = dict(indent=2, ensure_ascii=False, sort_keys=True, default=str) +dump = partial(json.dump, **json_dump_kwargs) +dumps = partial(json.dumps, **json_dump_kwargs) + + +class AttrDict(dict): + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + +# TODO: добавить defaults +class Config(dict): + def __init__(self, config_path: str | Path): + self._config_path = Path(config_path) + self._lock = Lock() + self.load() + + def load(self) -> None: + if self._config_path.exists(): + with self._lock: + with self._config_path.open() as f: + try: + self.update(json.load(f)) + except ValueError: + pass + + def save(self, *args: Any, **kwargs: Any) -> None: + self.update(*args, **kwargs) + self._config_path.parent.mkdir(exist_ok=True, parents=True) + with self._lock: + with self._config_path.open("w+") as f: + dump(self, f) + + __getitem__ = dict.get diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f9635b4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,379 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "black" +version = "23.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, + {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, + {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, + {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, + {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, + {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, + {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, + {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, + {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, + {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, + {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, + {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, + {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, + {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cchardet" +version = "2.1.7" +description = "cChardet is high speed universal character encoding detector." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "cchardet-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6f70139aaf47ffb94d89db603af849b82efdf756f187cdd3e566e30976c519f"}, + {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a25f9577e9bebe1a085eec2d6fdd72b7a9dd680811bba652ea6090fb2ff472f"}, + {file = "cchardet-2.1.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6b6397d8a32b976a333bdae060febd39ad5479817fabf489e5596a588ad05133"}, + {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:228d2533987c450f39acf7548f474dd6814c446e9d6bd228e8f1d9a2d210f10b"}, + {file = "cchardet-2.1.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:54341e7e1ba9dc0add4c9d23b48d3a94e2733065c13920e85895f944596f6150"}, + {file = "cchardet-2.1.7-cp36-cp36m-win32.whl", hash = "sha256:eee4f5403dc3a37a1ca9ab87db32b48dc7e190ef84601068f45397144427cc5e"}, + {file = "cchardet-2.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f86e0566cb61dc4397297696a4a1b30f6391b50bc52b4f073507a48466b6255a"}, + {file = "cchardet-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:302aa443ae2526755d412c9631136bdcd1374acd08e34f527447f06f3c2ddb98"}, + {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:70eeae8aaf61192e9b247cf28969faef00578becd2602526ecd8ae7600d25e0e"}, + {file = "cchardet-2.1.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a39526c1c526843965cec589a6f6b7c2ab07e3e56dc09a7f77a2be6a6afa4636"}, + {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b154effa12886e9c18555dfc41a110f601f08d69a71809c8d908be4b1ab7314f"}, + {file = "cchardet-2.1.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ec3eb5a9c475208cf52423524dcaf713c394393e18902e861f983c38eeb77f18"}, + {file = "cchardet-2.1.7-cp37-cp37m-win32.whl", hash = "sha256:50ad671e8d6c886496db62c3bd68b8d55060688c655873aa4ce25ca6105409a1"}, + {file = "cchardet-2.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:54d0b26fd0cd4099f08fb9c167600f3e83619abefeaa68ad823cc8ac1f7bcc0c"}, + {file = "cchardet-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b59ddc615883835e03c26f81d5fc3671fab2d32035c87f50862de0da7d7db535"}, + {file = "cchardet-2.1.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:27a9ba87c9f99e0618e1d3081189b1217a7d110e5c5597b0b7b7c3fedd1c340a"}, + {file = "cchardet-2.1.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:90086e5645f8a1801350f4cc6cb5d5bf12d3fa943811bb08667744ec1ecc9ccd"}, + {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:45456c59ec349b29628a3c6bfb86d818ec3a6fbb7eb72de4ff3bd4713681c0e3"}, + {file = "cchardet-2.1.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f16517f3697569822c6d09671217fdeab61dfebc7acb5068634d6b0728b86c0b"}, + {file = "cchardet-2.1.7-cp38-cp38-win32.whl", hash = "sha256:0b859069bbb9d27c78a2c9eb997e6f4b738db2d7039a03f8792b4058d61d1109"}, + {file = "cchardet-2.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:273699c4e5cd75377776501b72a7b291a988c6eec259c29505094553ee505597"}, + {file = "cchardet-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48ba829badef61441e08805cfa474ccd2774be2ff44b34898f5854168c596d4d"}, + {file = "cchardet-2.1.7-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bd7f262f41fd9caf5a5f09207a55861a67af6ad5c66612043ed0f81c58cdf376"}, + {file = "cchardet-2.1.7-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fdac1e4366d0579fff056d1280b8dc6348be964fda8ebb627c0269e097ab37fa"}, + {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:80e6faae75ecb9be04a7b258dc4750d459529debb6b8dee024745b7b5a949a34"}, + {file = "cchardet-2.1.7-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c96aee9ebd1147400e608a3eff97c44f49811f8904e5a43069d55603ac4d8c97"}, + {file = "cchardet-2.1.7-cp39-cp39-win32.whl", hash = "sha256:2309ff8fc652b0fc3c0cff5dbb172530c7abb92fe9ba2417c9c0bcf688463c1c"}, + {file = "cchardet-2.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:24974b3e40fee9e7557bb352be625c39ec6f50bc2053f44a3d1191db70b51675"}, + {file = "cchardet-2.1.7.tar.gz", hash = "sha256:c428b6336545053c2589f6caf24ea32276c6664cb86db817e03a94c60afa0eaf"}, +] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.0.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] + +[[package]] +name = "pathspec" +version = "0.11.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] + +[[package]] +name = "platformdirs" +version = "3.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, + {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "36aba2bee33b527dc0eb0d2ba5c1a5a905374c0d62efbb8fd29eb28576679ba7" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8d8049b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "hh-applicant-tool" +version = "0.1.0" +description = "" +authors = ["Senior YAML Developer "] +readme = "README.md" +packages = [{include = "hh_applicant_tool"}] + +[tool.poetry.dependencies] +python = "^3.10" +python-dotenv = "^1.0.0" +requests = "^2.28.2" +cchardet = "^2.1.7" + + +[tool.poetry.group.dev.dependencies] +black = "^23.1.0" +isort = "^5.12.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +hh-applicant-tool = "hh_applicant_tool.main:main" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29