diff --git a/app_config.py b/app_config.py new file mode 100644 index 0000000..5fa1715 --- /dev/null +++ b/app_config.py @@ -0,0 +1,64 @@ +"""Загрузка настроек приложения. Используется библиотека `configparser`.""" +import configparser +from os import path + + +class AppConfig: + """Класс для хранения настроек сервиса. + TODO: Дописать документацию! + """ + + def __init__(self, path_to_conf_file: str, section: str = "Main"): + """ + Parameters + ---------- + path_to_conf_file: str + Путь к файлу конфига. Можно указывать относительный. + + section: str + Секция в конфиге, которую нужно считывать. По-умолчанию секция `[Main]`. + + Raises + ------ + KeyError + Если в конфиге не найдено указанной секции. + + + """ + cfg: configparser = configparser.ConfigParser() + + try: + cfg.read(path.abspath(path_to_conf_file)) + except FileNotFoundError: + print(f"File {path.abspath(path_to_conf_file)} not found!") + + if section not in cfg.sections(): + raise KeyError(f"Section {section} not found in config file!") + + conf = dict(cfg.items(section)) + + self.template: str = conf["template"] + self.path_for_config: str = conf["path_for_config"] + self.frequency_sec = int(conf["frequency_sec"]) + self.central_host_url: str = conf["central_host_url"] + self.requests_count: int = int(conf["requests_count"]) + self.request_portion: int = int(conf["request_portion"]) + + @property + def configs_path(self) -> str: + """Возвращает абсолютный путь до папки с конфигами.""" + _path = path.abspath(self.path_for_config) + return _path + + def _create_path(self) -> None: + pass + + def __repr__(self): + return ( + f"template = {self.template}\n" + f"path_for_config = {self.path_for_config}\n" + f"{self.frequency_sec}\n" + f"{self.central_host_url}\n" + f"{self.requests_count}\n" + f"{self.request_portion}\n" + ) diff --git a/config_object.py b/config_object.py new file mode 100644 index 0000000..345abb2 --- /dev/null +++ b/config_object.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +import asyncio +import os + + +@dataclass +class ConfigObject: + host: str + conf_body: str + path: str + + @property + def existst(self) -> bool: + """Возвращает True, если файл конфига уже существует.""" + return path.isfile(path) + + async def write(self) -> bool: + """Записывает конфиг файл.""" + _config_file_name: str = path.join(path, f"{host}.conf") + + if not self.existst: + pass + return True + + +def read_config_template_file(path_to_file: str) -> str: + """Прочесть шаблон конфига для сервера из файла.""" + template: str = "" + _full_path = os.path.abspath(path_to_file) + with open(_full_path, mode="r", encoding="utf8") as file: + template = file.read() + + return template diff --git a/request_builder.py b/request_builder.py new file mode 100644 index 0000000..2ab25d8 --- /dev/null +++ b/request_builder.py @@ -0,0 +1,47 @@ +"""Модлуль для конструирования и обработки запросов.""" + +from app_config import AppConfig +from aiohttp import ClientResponse +import asyncio +import aiohttp + + +class RequestBulder: + def __init__(self, cfg: AppConfig): + self.cfg = cfg + + _conn = aiohttp.TCPConnector(limit=cfg.requests_count) + self.session = aiohttp.ClientSession(connector=_conn) + + async def __check_resp(self, resp: ClientResponse) -> bool: + if not resp.status == 200: + return False + + response = await resp.json() + + if not response["done"]: + return False + + return True + + async def send_request(self, url, json_body) -> dict | bool: + """Выполняет запрос, при успехе возвращает json с ответом, при + неудаче возвращает False.""" + _url = f"{self.cfg.central_host_url}/{url}" + + raw_resp = await self.session.get(_url, json=json_body) + + if await self.__check_resp(raw_resp): + json_resp = await raw_resp.json() + json_resp.pop("done") + return json_resp + + return False + + async def wait(self) -> None: + """Ждет frequency_sec время.""" + await asyncio.sleep(self.cfg.frequency_sec) + + # def __del__(self): + # if self.session is not None: + # self.session.close() diff --git a/solution.py b/solution.py index 1aa91cb..bff9960 100644 --- a/solution.py +++ b/solution.py @@ -1,104 +1,139 @@ -import asyncio -import requests -import json +from dataclasses import dataclass +from asyncio import Task from typing import List +from aiohttp.client_exceptions import ClientConnectorError + +import asyncio import os -import configparser +import aiohttp +import aiofiles +from config_object import ConfigObject, read_config_template_file +from app_config import AppConfig +from request_builder import RequestBulder -async def write_config_file( - server_name: str, path_to_template_file: str, path_for_config: str -) -> None: - template: str = "" - with open(path_to_template_file, "r") as file: - template = file.read() - config_body: str = template.replace( - "server_name _;", f"server_name {server_name}.server.com;" - ) - condifg_full_path: str = os.path.abspath(path_for_config) - config_filename: str = f"{server_name}.conf" +async def write_config_files(hosts: list, cfg: AppConfig, template: str) -> None: + """Записываем конфиги для списка hosts.""" - if not os.path.isdir(condifg_full_path): - os.mkdir(condifg_full_path) + full_path_to_config_dir: str = os.path.abspath(cfg.path_for_config) - with open(os.path.join(condifg_full_path, config_filename), "w") as file: - file.write(config_body) + if not os.path.isdir(full_path_to_config_dir): + os.mkdir(full_path_to_config_dir) + for host in hosts: + config_filename: str = f"{host}.conf" + config_file = os.path.join(full_path_to_config_dir, config_filename) -async def send_request(server: str, columns: list, limit: int = 1) -> dict: - response = requests.get(server, json={"columns": columns, "limit": limit}) + if not os.path.isfile(config_file): + config_body: str = template.replace( + "server_name _;", f"server_name {host}.server.com;" + ) + async with aiofiles.open(config_file, mode="w") as file: + await file.write(config_body) + + +def _get_template(templ_file: str) -> str: + """Возвращает шаблон конфига для хоста из файла `templ_file`.""" + + template: str = "" + with open(templ_file, mode="r", encoding="utf8") as file: + template = file.read() - return response.json() + return template -async def get_hosts(server_response: dict) -> List[str]: - """Получить хосты из ответа сервера. +async def get_records_count(session, server) -> int: + """Возвращает количество записей в базе.""" + async with session.get(f"{server}/count") as resp: + resp = await resp.json() + count: int = int(resp["result"]) + return count + + +async def get_tasks( + url: str, portion: int, count: int, session: aiohttp.ClientSession, body: dict +) -> List[Task]: + """Вернет список задач с запросами к API. + + Функция не ограничивает кол-во запросов, это нужно сделать до + вызова, чтобы передать корректный `session`. Parameters ---------- - server_response : dict + url : str + Куда слать запрос. Ожидается "server/get" + portion : int + Сколько записей запрашивать за раз + count : int + Общее количество записей + session : aiohttp.ClientSession + Объект сессии, создается в уровне выше, с одним объектом + меньше накладных расходов на каждый запрос. + body : json + Json для запроса. """ + tasks: List[Task] = [] - hosts: list = [] + for offset in range(0, count, portion): + tasks.append(asyncio.create_task(session.get(url, json=body))) + body["offset"] = offset - for host in server_response.get("result"): - hosts.append(host.get("hostname")) + return tasks - return hosts +async def send_async_request(cfg: AppConfig, json_body: dict) -> None: + """Начать серию запросов.""" + template: str = _get_template(cfg.template) -async def read_config(path_to_conf_file: str, section: str = "Main") -> dict: - """ - Считать конфиг с помощью `configparser`. + # ограничим одновременное число запросов + conn = aiohttp.TCPConnector(limit=cfg.requests_count) - Parameters - ---------- - path_to_conf_file: str - Путь к файлу конфига. Можно указывать относительный. + url = cfg.central_host_url + portion = cfg.request_portion - section: str - Секция в конфиге, которую нужно считывать. По-умолчанию секция [Main]. + try: + async with aiohttp.ClientSession(connector=conn) as session: + # получаем количесвто записей + count = await get_records_count(session, url) - Raises - ------ - KeyError - Если в конфиге не найдено указанной секции. + tasks = await get_tasks(f"{url}/get", portion, count, session, json_body) - Returns - ------- - dict - Словарь, из значений указанной секции. - """ - config = configparser.ConfigParser() - config.read(os.path.abspath(path_to_conf_file)) + responses = await asyncio.gather(*tasks) - if section not in config.sections(): - raise KeyError(f"Section {section} not found in config file!") + # Пройдемся по ответам на запросы, запишем файлы конфига для + # каждого respone. Каждый response содержит portion или меньше хостов + for response in responses: + resp = await response.json() + hosts = [i["hostname"] for i in resp.get("result")] + await write_config_files(hosts, cfg, template) + except ClientConnectorError: + print(f"Невозможно подключиться к серверу {url}") - return dict(config.items(section)) +async def main() -> None: + """Точка входа.""" + cfg: AppConfig = AppConfig("service.conf") + print(cfg.configs_path) -async def main(): + rb = RequestBulder(cfg) - cnf = await read_config("service.conf") + resp = await rb.send_request("count", json_body={}) - wait_sec: int = int(cnf["frequency_sec"]) + if resp: + records_count = resp.get("result") - while True: - resp = await send_request( - cnf["central_host_url"], columns=["hostname"], limit=9 - ) + print(records_count) - hosts = await get_hosts(resp) + body_json = {"columns": "['hostname']", "limit": cfg.request_portion, "offset": 0} - for host in hosts: - await write_config_file( - server_name=host, - path_to_template_file=cnf["template"], - path_for_config=cnf["path_for_config"], - ) - await asyncio.sleep(wait_sec) + template = read_config_template_file(cfg.template) + + print(template) + # while True: + + # await send_async_request(cfg, json_body=body) + # await asyncio.sleep(cfg.frequency_sec) if __name__ == "__main__":