diff --git a/.env.sample b/.env.sample index 9fd2e4b..8cd0f10 100644 --- a/.env.sample +++ b/.env.sample @@ -13,6 +13,8 @@ LOGGING_LEVEL=DEBUG API_KEY_APILAYER= # https://openweathermap.org/price#weather API_KEY_OPENWEATHER= +# https://newsapi.org +API_KEY_NEWSPORTAL= # время актуальности данных о странах (в секундах) CACHE_TTL_COUNTRY=31_536_000 diff --git a/Dockerfile b/Dockerfile index 7b9b8b0..2df4865 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* COPY ./requirements.txt ./setup.cfg ./black.toml ./.pylintrc / +COPY ./pytest.ini /pytest.ini RUN --mount=type=cache,target=/root/.cache/pip \ pip install --upgrade pip -r /requirements.txt diff --git a/README.md b/README.md index b731e52..f6dbf44 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Install the appropriate software: Set received access tokens as environment variable values (in `.env` file): - `API_KEY_APILAYER` – for APILayer access token - `API_KEY_OPENWEATHER` – for OpenWeather access token + - `API_KEY_NEWSPORTAL` - for NewsAPI access token 2. Build the container using Docker Compose: ```shell @@ -62,6 +63,7 @@ Install the appropriate software: - `CACHE_TTL_COUNTRY` (country data up-to-date time in seconds) - `CACHE_TTL_CURRENCY_RATES` (currency rates data up-to-date time in seconds) - `CACHE_TTL_WEATHER` (weather data up-to-date time in seconds) + - `CACHE_TTL_NEWS` (news data up-to-date time in seconds) 5. After collecting all the data, you can query the country information by executing the command: ```shell diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..5dcd0c8 --- /dev/null +++ b/Report.md @@ -0,0 +1,20 @@ + +- [Что было сделано](#Что-было-сделано) + - [Таблица](#Таблица) + - [Тесты](#Тесты) + +## Что было сделано + +- Реализован сбор новостей +- Сделан вывод в таблицу +- Добавлены автотесты + - И pytest.ini добавлен в контейнер +- Обновлена документация +![](https://raw.githubusercontent.com/makspepe/python-course-country-directory/hw2/media/1.PNG) + +### Таблица +![](https://raw.githubusercontent.com/makspepe/python-course-country-directory/hw2/media/2.PNG) +![](https://raw.githubusercontent.com/makspepe/python-course-country-directory/hw2/media/3.PNG) + +### Тесты +![](https://raw.githubusercontent.com/makspepe/python-course-country-directory/hw2/media/4.PNG) diff --git a/media/.gitignore b/media/.gitignore index 67c04e4..845578e 100644 --- a/media/.gitignore +++ b/media/.gitignore @@ -1,2 +1,6 @@ *.* !.gitignore +!1.PNG +!2.PNG +!3.PNG +!4.PNG diff --git a/media/1.PNG b/media/1.PNG new file mode 100644 index 0000000..f97efb1 Binary files /dev/null and b/media/1.PNG differ diff --git a/media/2.PNG b/media/2.PNG new file mode 100644 index 0000000..3d749b9 Binary files /dev/null and b/media/2.PNG differ diff --git a/media/3.PNG b/media/3.PNG new file mode 100644 index 0000000..c4e6b0f Binary files /dev/null and b/media/3.PNG differ diff --git a/media/4.PNG b/media/4.PNG new file mode 100644 index 0000000..91e8ec3 Binary files /dev/null and b/media/4.PNG differ diff --git a/requirements.txt b/requirements.txt index 138eaea..55571fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,6 @@ mypy>=0.971,<1.0 # автоматическое форматирование кода black>=22.8.0,<22.9.0 + +# таблицы +tabulate>=0.8.0,<1.0 diff --git a/src/clients/base.py b/src/clients/base.py index cf8f5b0..960963f 100644 --- a/src/clients/base.py +++ b/src/clients/base.py @@ -25,5 +25,5 @@ async def _request(self, endpoint: str) -> Optional[dict]: Формирование и выполнение запроса. :param endpoint: - :return: - """ + :return: JSON + """ \ No newline at end of file diff --git a/src/clients/country.py b/src/clients/country.py index 65a30b0..2fd4416 100644 --- a/src/clients/country.py +++ b/src/clients/country.py @@ -36,7 +36,7 @@ async def get_countries(self, bloc: str = "eu") -> Optional[dict]: Получение данных о странах. :param bloc: Регион - :return: + :return: JSON о странах региона """ return await self._request(f"{await self.get_base_url()}/regional_bloc/{bloc}") diff --git a/src/clients/currency.py b/src/clients/currency.py index 3c89d8f..0cabd5d 100644 --- a/src/clients/currency.py +++ b/src/clients/currency.py @@ -36,7 +36,7 @@ async def get_rates(self, base: str = "rub") -> Optional[dict]: Получение данных о курсах валют. :param base: Базовая валюта - :return: + :return: JSON о курсах валют """ return await self._request(f"{await self.get_base_url()}?base={base}") diff --git a/src/clients/news.py b/src/clients/news.py new file mode 100644 index 0000000..1b02f20 --- /dev/null +++ b/src/clients/news.py @@ -0,0 +1,45 @@ +""" +Функции для взаимодействия с внешним сервисом-провайдером новостей. +""" +from http import HTTPStatus +from typing import Optional + +import aiohttp + +from clients.base import BaseClient +from logger import trace_config +from settings import API_KEY_NEWSPORTAL + + +class NewsClient(BaseClient): + """ + Реализация функций для взаимодействия с внешним сервисом-провайдером новостей. + """ + + async def get_base_url(self) -> str: + """ + Возвращает базовый URL для API новостей. + :return: + """ + return "https://newsapi.org/v2/everything" + + async def _request(self, endpoint: str) -> Optional[dict]: + """ + Отправляет запрос HTTP GET на указанную конечную точку и возвращает ответ в виде JSON. + """ + + async with aiohttp.ClientSession(trace_configs=[trace_config]) as session: + async with session.get(endpoint) as response: + if response.status == HTTPStatus.OK: + return await response.json() + return None + + async def get_news(self, country: str) -> Optional[dict]: + """ + Получение новостей по заданной стране, опубликованных сегодня. + :param country: Страна с которой собирается новость + :return: JSON о новостях + """ + + endpoint = f"{await self.get_base_url()}?q={country}&sortBy=publishedAt&apiKey={API_KEY_NEWSPORTAL}" + return await self._request(endpoint) diff --git a/src/clients/weather.py b/src/clients/weather.py index b39eebf..706fd12 100644 --- a/src/clients/weather.py +++ b/src/clients/weather.py @@ -33,7 +33,7 @@ async def get_weather(self, location: str) -> Optional[dict]: Получение данных о погоде. :param location: Город и страна - :return: + :return: JSON о погоде в заданном городе """ return await self._request( diff --git a/src/collectors/base.py b/src/collectors/base.py index 29b2d78..d2bb070 100644 --- a/src/collectors/base.py +++ b/src/collectors/base.py @@ -16,17 +16,26 @@ class BaseCollector(ABC): @abstractmethod async def collect(self, **kwargs: Any) -> Optional[Iterable[Any]]: - ... + """ + Получение данных из JSON. + :return: Коллекция данных + """ @staticmethod @abstractmethod async def get_file_path(**kwargs: Any) -> str: - ... + """ + Получение пути к файлу. + :return: + """ @staticmethod @abstractmethod async def get_cache_ttl() -> int: - ... + """ + Получение времени жизни кэша. + :return: + """ async def cache_invalid(self, **kwargs: Any) -> bool: """ diff --git a/src/collectors/collector.py b/src/collectors/collector.py index ebadf7e..6ee643b 100644 --- a/src/collectors/collector.py +++ b/src/collectors/collector.py @@ -13,12 +13,14 @@ from clients.country import CountryClient from clients.currency import CurrencyClient +from clients.news import NewsClient from clients.weather import WeatherClient from collectors.base import BaseCollector from collectors.models import ( LocationDTO, CountryDTO, CurrencyRatesDTO, + NewsDTO, CurrencyInfoDTO, WeatherInfoDTO, ) @@ -26,6 +28,7 @@ MEDIA_PATH, CACHE_TTL_COUNTRY, CACHE_TTL_CURRENCY_RATES, + CACHE_TTL_NEWS, CACHE_TTL_WEATHER, ) @@ -47,6 +50,9 @@ async def get_cache_ttl() -> int: return CACHE_TTL_COUNTRY async def collect(self, **kwargs: Any) -> Optional[FrozenSet[LocationDTO]]: + """ + Получение данных из JSON. + """ if await self.cache_invalid(): # если кэш уже невалиден, то актуализируем его result = await self.client.get_countries() @@ -63,6 +69,7 @@ async def collect(self, **kwargs: Any) -> Optional[FrozenSet[LocationDTO]]: if result: locations = frozenset( LocationDTO( + country=item["name"], capital=item["capital"], alpha2code=item["alpha2code"], ) @@ -90,7 +97,10 @@ async def read(cls) -> Optional[list[CountryDTO]]: for item in items: result_list.append( CountryDTO( + name=item["name"], capital=item["capital"], + capital_latitude=item["latitude"], + capital_longitude=item["longitude"], alpha2code=item["alpha2code"], alt_spellings=item["alt_spellings"], currencies={ @@ -98,8 +108,8 @@ async def read(cls) -> Optional[list[CountryDTO]]: for currency in item["currencies"] }, flag=item["flag"], + area=item["area"], languages=item["languages"], - name=item["name"], population=item["population"], subregion=item["subregion"], timezones=item["timezones"], @@ -203,7 +213,7 @@ async def read(cls, location: LocationDTO) -> Optional[WeatherInfoDTO]: """ Чтение данных из кэша. - :param location: + :param location: Город для получения данных :return: """ @@ -214,17 +224,24 @@ async def read(cls, location: LocationDTO) -> Optional[WeatherInfoDTO]: result = json.loads(content) if result: return WeatherInfoDTO( + date_time=result["dt"], + utc_timezone=result["timezone"], temp=result["main"]["temp"], pressure=result["main"]["pressure"], humidity=result["main"]["humidity"], wind_speed=result["wind"]["speed"], description=result["weather"][0]["description"], + visibility=result["visibility"], ) return None class Collectors: + """ + Сборщик данных. + """ + @staticmethod async def gather() -> tuple: return await asyncio.gather( @@ -238,7 +255,84 @@ def collect() -> None: try: results = loop.run_until_complete(Collectors.gather()) loop.run_until_complete(WeatherCollector().collect(results[1])) + loop.run_until_complete(NewsCollector().collect(results[1])) loop.run_until_complete(loop.shutdown_asyncgens()) finally: loop.close() + + +class NewsCollector(BaseCollector): + """ + Собирает новости по указанным странам. + """ + + def __init__(self) -> None: + self.client = NewsClient() + + @staticmethod + async def get_file_path(filename: str = "", **kwargs: Any) -> str: + """ + Возвращает путь к файлу для хранения данных новостей JSON. + """ + return f"{MEDIA_PATH}/news/{filename}.json" + + @staticmethod + async def get_cache_ttl() -> int: + """ + Возвращает TTL (время жизни) кэша для новостных данных. + """ + return CACHE_TTL_NEWS + + async def collect( + self, locations: FrozenSet[LocationDTO] = frozenset(), **kwargs: Any + ) -> None: + """ + Собирает данные о новостях для указанных стран и сохраняет их в JSON. + """ + target_dir_path = f"{MEDIA_PATH}/news" + # если целевой директории еще не существует, то она создается + if not await aiofiles.os.path.exists(target_dir_path): + await aiofiles.os.mkdir(target_dir_path) + + for location in locations: + filename = f"{location.country}".lower() + + # если кэш уже невалиден, то актуализируем его + if await self.cache_invalid(filename=filename): + result = await self.client.get_news(location.country) + if result: + # сохраняем данные в JSON + result_str = json.dumps(result) + async with aiofiles.open( + await self.get_file_path(filename), mode="w" + ) as file: + await file.write(result_str) + + @classmethod + async def read(cls, location: LocationDTO, number: int) -> Optional[NewsDTO]: + """ + Чтение данных из кэша. + :param location: Локация для получения данных + :param number: Порядковый номер новости + :return: + """ + filename = f"{location.country}".lower() + + # читаем данные из JSON + async with aiofiles.open(await cls.get_file_path(filename), mode="r") as file: + content = await file.read() + + result = json.loads(content) + if result: + # возвращаем данные о новости + article = result["articles"][number] + return NewsDTO( + source=article["source"]["name"], + author=article["author"], + published_at=article["publishedAt"], + title=article["title"], + description=article["description"], + ) + + return None diff --git a/src/collectors/models.py b/src/collectors/models.py index 7e36198..5006f27 100644 --- a/src/collectors/models.py +++ b/src/collectors/models.py @@ -2,7 +2,9 @@ Описание моделей данных (DTO). """ -from pydantic import Field, BaseModel +from pydantic import Field, BaseModel, validator +from datetime import datetime +from typing import Optional, Union class HashableBaseModel(BaseModel): @@ -21,11 +23,13 @@ class LocationDTO(HashableBaseModel): .. code-block:: LocationDTO( + country="Aland", capital="Mariehamn", alpha2code="AX", ) """ + country: str capital: str alpha2code: str = Field(min_length=2, max_length=2) # country alpha‑2 code @@ -68,6 +72,8 @@ class CountryDTO(BaseModel): CountryDTO( capital="Mariehamn", + capital_latitude=20.55, + capital_longitude=13.44, alpha2code="AX", alt_spellings=[ "AX", @@ -89,6 +95,7 @@ class CountryDTO(BaseModel): }, name="\u00c5land Islands", population=28875, + area=1555.0, subregion="Northern Europe", timezones=[ "UTC+02:00", @@ -97,6 +104,8 @@ class CountryDTO(BaseModel): """ capital: str + capital_latitude: float + capital_longitude: float alpha2code: str alt_spellings: list[str] currencies: set[CurrencyInfoDTO] @@ -104,6 +113,7 @@ class CountryDTO(BaseModel): languages: set[LanguagesInfoDTO] name: str population: int + area: Optional[float] subregion: str timezones: list[str] @@ -140,14 +150,49 @@ class WeatherInfoDTO(BaseModel): humidity=54, wind_speed=4.63, description="scattered clouds", + visibility=5000, + utc_timezone=3600, + date_time=2023-02-25 13:37:00 + ) ) """ + date_time: datetime + utc_timezone: int temp: float pressure: int humidity: int wind_speed: float description: str + visibility: int + + +class NewsDTO(BaseModel): + """ + Модель данных о новости. + + .. code-block:: + + NewsDTO( + source="ABC News", + author="STEVE KARNOWSKI Associated Press", + published_at=2023-02-25 02:22:40, + title="PolyMet mine in Minnesota becomes NewRange Copper Nickel again", + description="sample text" + ) + """ + + source: str + author: Union[str, None] + published_at: datetime + title: str + description: str + + @validator("author") + def validate_author(cls, value): + if value is None or value == "": + return "unknown" + return value class LocationInfoDTO(BaseModel): @@ -180,6 +225,7 @@ class LocationInfoDTO(BaseModel): }, name="\u00c5land Islands", population=28875, + area=1555.0, subregion="Northern Europe", timezones=[ "UTC+02:00", @@ -191,13 +237,19 @@ class LocationInfoDTO(BaseModel): humidity=54, wind_speed=4.63, description="scattered clouds", + visibility=10000, + utc_timezone=3600, + date_time=2023-02-25 13:37:00 ), currency_rates={ "EUR": 0.016503, }, + capital_latitude=20.55, + capital_longitude=13.44, ) """ location: CountryDTO weather: WeatherInfoDTO currency_rates: dict[str, float] + country_news: Optional[list[NewsDTO]] diff --git a/src/reader.py b/src/reader.py index dd1a74d..d3344a6 100644 --- a/src/reader.py +++ b/src/reader.py @@ -8,6 +8,7 @@ from collectors.collector import ( CountryCollector, CurrencyRatesCollector, + NewsCollector, WeatherCollector, ) from collectors.models import ( @@ -15,6 +16,7 @@ CurrencyInfoDTO, LocationDTO, LocationInfoDTO, + NewsDTO, WeatherInfoDTO, ) @@ -24,25 +26,30 @@ class Reader: Чтение сохраненных данных. """ - async def find(self, location: str) -> Optional[LocationInfoDTO]: + async def find(self, name: str) -> Optional[LocationInfoDTO]: """ Поиск данных о стране по строке. - :param location: Строка для поиска + :param name: Строка для поиска :return: """ - country = await self.find_country(location) + country = await self.find_country(name) if country: - weather = await self.get_weather( - LocationDTO(capital=country.capital, alpha2code=country.alpha2code) + location = LocationDTO( + capital=country.capital, + alpha2code=country.alpha2code, + country=country.name, ) + weather = await self.get_weather(location) currency_rates = await self.get_currency_rates(country.currencies) + news = await self.get_news(location) return LocationInfoDTO( location=country, weather=weather, currency_rates=currency_rates, + country_news=news, ) return None @@ -65,6 +72,19 @@ async def get_currency_rates(currencies: set[CurrencyInfoDTO]) -> dict[str, floa return result + @staticmethod + async def get_news(location: LocationDTO) -> list[NewsDTO]: + """ + Получение данных о новостях. + :param location: Место для получения данных + :return: + """ + return [ + await NewsCollector.read(location=location, number=i) + for i in range(3) + if i < len(location.news_sources) + ] + @staticmethod async def get_weather(location: LocationDTO) -> Optional[WeatherInfoDTO]: """ diff --git a/src/renderer.py b/src/renderer.py index 8b90dcc..bc553b9 100644 --- a/src/renderer.py +++ b/src/renderer.py @@ -4,6 +4,8 @@ from decimal import ROUND_HALF_UP, Decimal +from tabulate import SEPARATING_LINE, tabulate + from collectors.models import LocationInfoDTO @@ -12,6 +14,9 @@ class Renderer: Генерация результата преобразования прочитанных данных. """ + WEATHER_HEADERS = ["Weather", ""] + NEWS_HEADERS = ["Latest news about", ""] + def __init__(self, location_info: LocationInfoDTO) -> None: """ Конструктор. @@ -21,23 +26,65 @@ def __init__(self, location_info: LocationInfoDTO) -> None: self.location_info = location_info - async def render(self) -> tuple[str, ...]: + async def render(self) -> None: """ - Форматирование прочитанных данных. + Форматирование прочитанных данных, вывод в консоль. - :return: Результат форматирования + :return: None """ - return ( - f"Страна: {self.location_info.location.name}", - f"Столица: {self.location_info.location.capital}", - f"Регион: {self.location_info.location.subregion}", - f"Языки: {await self._format_languages()}", - f"Население страны: {await self._format_population()} чел.", - f"Курсы валют: {await self._format_currency_rates()}", - f"Погода: {self.location_info.weather.temp} °C", + table = [ + ["Страна", self.location_info.location.name], + ["Столица", self.location_info.location.capital], + [ + "Координаты столицы", + f"{self.location_info.location.capital_latitude}; {self.location_info.location.capital_longitude}", + ], + ["Регион", self.location_info.location.subregion], + ["Языки", await self._format_languages()], + ["Площадь", f"{self.location_info.location.area} кв. км."], + ["Население страны", f"{await self._format_population()} чел."], + ["Курсы валют", await self._format_currency_rates()], + ["Часовой пояс", self.location_info.weather.utc_timezone], + ] + + table.extend( + [ + self.WEATHER_HEADERS, + SEPARATING_LINE, + ["Погода", self.location_info.weather.description], + ["Температура", f"{self.location_info.weather.temp} °C"], + ["Скорость ветра", f"{self.location_info.weather.wind_speed} м.с."], + ["Видимость", f"{self.location_info.weather.visibility} м."], + [ + "Время получения данных", + self.location_info.weather.date_time.strftime("%d.%m.%Y %H:%M"), + ], + ] ) + if len(self.location_info.country_news) > 0: + table.extend( + [ + self.NEWS_HEADERS, + SEPARATING_LINE, + ] + ) + for item in self.location_info.country_news: + table.extend( + [ + [item.title, ""], + ["Источник", item.source], + [ + "Дата публикации", + item.published_at.strftime("%d.%m.%Y %H:%M"), + ], + SEPARATING_LINE, + ] + ) + + print(tabulate(table, ["General", ""], tablefmt="simple")) + async def _format_languages(self) -> str: """ Форматирование информации о языках. diff --git a/src/settings.py b/src/settings.py index 4559f84..26374d0 100644 --- a/src/settings.py +++ b/src/settings.py @@ -20,10 +20,13 @@ # ключи для доступа к API API_KEY_APILAYER: Optional[str] = os.getenv("API_KEY_APILAYER") API_KEY_OPENWEATHER: Optional[str] = os.getenv("API_KEY_OPENWEATHER") +API_KEY_NEWSPORTAL: Optional[str] = os.getenv("API_KEY_NEWSPORTAL") # время актуальности данных о странах (в секундах), по умолчанию – один год CACHE_TTL_COUNTRY: int = int(os.getenv("CACHE_TTL_COUNTRY", "31_536_000")) # время актуальности данных о курсах валют (в секундах), по умолчанию – сутки CACHE_TTL_CURRENCY_RATES: int = int(os.getenv("CACHE_TTL_CURRENCY_RATES", "86_400")) +# время актуальности данных о новостях (в секундах), по умолчанию ~ один час +CACHE_TTL_NEWS: int = int(os.getenv("CACHE_TTL_NEWS", "3600")) # время актуальности данных о погоде (в секундах), по умолчанию ~ три часа CACHE_TTL_WEATHER: int = int(os.getenv("CACHE_TTL_WEATHER", "10_700")) diff --git a/src/tests/clients/test_country.py b/src/tests/clients/test_country.py index b04944d..19382b2 100644 --- a/src/tests/clients/test_country.py +++ b/src/tests/clients/test_country.py @@ -6,7 +6,6 @@ from clients.country import CountryClient - @pytest.mark.asyncio class TestClientCountry: """ diff --git a/src/tests/clients/test_currency.py b/src/tests/clients/test_currency.py index 104d612..0b78b29 100644 --- a/src/tests/clients/test_currency.py +++ b/src/tests/clients/test_currency.py @@ -1,3 +1,32 @@ """ -Тестирование функций клиента для получения информации о курсах валют. +Тестирование клиента для получения информации о валютах. """ + +import pytest + +from clients.currency import CurrencyClient + + +class TestClientCountry: + """ + Тестирование клиента для получения информации о валютах. + """ + + base_url = "https://api.apilayer.com/fixer/latest" + + @pytest.fixture + def client(self): + return CurrencyClient() + + async def test_get_base_url(self, client): + assert await client.get_base_url() == self.base_url + + async def test_get_countries(self, mocker, client): + mocker.patch("clients.base.BaseClient._request") + await client.get_rates() + client._request.assert_called_once_with(self.base_url) + assert client.params == {"base": "rub"} + + await client.get_rates("test") + client._request.assert_called_with(self.base_url) + assert client.params == {"base": "test"} diff --git a/src/tests/clients/test_weather.py b/src/tests/clients/test_weather.py index 3c14430..412ce82 100644 --- a/src/tests/clients/test_weather.py +++ b/src/tests/clients/test_weather.py @@ -1,3 +1,29 @@ """ Тестирование функций клиента для получения информации о погоде. """ + + +import pytest + +from clients.weather import WeatherClient + + +class TestClientCountry: + """ + Тестирование клиента для получения информации о странах. + """ + + base_url = "https://api.openweathermap.org/data/2.5/weather" + + @pytest.fixture + def client(self): + return WeatherClient() + + async def test_get_base_url(self, client): + assert await client.get_base_url() == self.base_url + + async def test_get_countries(self, mocker, client): + mocker.patch("clients.base.BaseClient._request") + await client.get_weather("test") + client._request.assert_called_with(self.base_url) + assert client.params["q"] == "test" diff --git a/src/tests/collectors/test_country.py b/src/tests/collectors/test_country.py index 325936d..9277e71 100644 --- a/src/tests/collectors/test_country.py +++ b/src/tests/collectors/test_country.py @@ -1,3 +1,26 @@ """ Тестирование функций сбора информации о странах. """ + +import pytest + +from collectors.collector import CountryCollector + + +@pytest.mark.asyncio +class TestCollectorCountry: + """ + Тестирование сбора информации о странах. + """ + + base_url = "https://api.apilayer.com/geo/country" + + @pytest.fixture + def collector(self): + return CountryCollector() + + async def test_collect(self, collector): + assert await collector.collect() is not None + + async def test_read(cls, collector): + assert await collector.read() is not None diff --git a/src/tests/collectors/test_currency.py b/src/tests/collectors/test_currency.py index f8982e0..27c2ade 100644 --- a/src/tests/collectors/test_currency.py +++ b/src/tests/collectors/test_currency.py @@ -1,3 +1,36 @@ """ Тестирование функций сбора информации о курсах валют. """ + +""" +Тестирование функций сбора информации о курсах валют. +""" + +import pytest + +from collectors.collector import CurrencyRatesCollector + + +class TestCurrencyCollector: + """ + Тестирование функций сбора информации о курсах валют. + """ + + @pytest.fixture(autouse=True) + def setup(self): + self.collector = CurrencyRatesCollector() + + async def test_collect_currency_success(self): + """ + Тестирование получения информации о курсе валют. + """ + await self.collector.collect() + + async def test_read_currency_success(self): + """ + Тестирование чтения информации о курсе валют. + """ + currencies = await self.collector.read() + assert currencies is not None + assert currencies.base == "RUB" + assert len(currencies.rates) == 170