Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ LOGGING_LEVEL=DEBUG
API_KEY_APILAYER=
# https://openweathermap.org/price#weather
API_KEY_OPENWEATHER=
# https://newsapi.org/
API_KEY_NEWS=

# время актуальности данных о странах (в секундах)
CACHE_TTL_COUNTRY=31_536_000
# время актуальности данных о курсах валют (в секундах)
CACHE_TTL_CURRENCY_RATES=86_400
# время актуальности данных о погоде (в секундах)
CACHE_TTL_WEATHER=10_700
# время актуальности данных о новостях (в секундах)
CACHE_TTL_NEWS=3_600
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ Install the appropriate software:
To access the API, visit the appropriate resources and obtain an access token:
- APILayer – Geography API (https://apilayer.com/marketplace/geo-api)
- OpenWeather – Weather Free Plan (https://openweathermap.org/price#weather)
- NewsAPI - Free News API for all countries (https://newsapi.org/)

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_NEWS` – for NewsAPI access token

2. Build the container using Docker Compose:
```shell
Expand All @@ -62,6 +64,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 up-to-date time in seconds)

5. After collecting all the data, you can query the country information by executing the command:
```shell
Expand Down
3 changes: 3 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@

* APILayer — Geography API (https://apilayer.com/marketplace/geo-api)
* OpenWeather – Weather Free Plan (https://openweathermap.org/price#weather)
* NewsAPI - Free News API for all countries (https://newsapi.org/)

Задайте полученные токены доступа в качестве значений переменных окружения (в файле `.env`):

* `API_KEY_APILAYER` – для токена доступа к APILayer
* `API_KEY_OPENWEATHER` – для токена доступа к OpenWeather
* `API_KEY_NEWS` – for NewsAPI access token

2. Соберите Docker-контейнер с помощью Docker Compose:
.. code-block:: console
Expand Down Expand Up @@ -92,6 +94,7 @@
* `CACHE_TTL_COUNTRY` (время актуальности данных о странах)
* `CACHE_TTL_CURRENCY_RATES` (время актуальности данных о курсах валют)
* `CACHE_TTL_WEATHER` (время актуальности данных о погоде)
* `CACHE_TTL_NEWS` (news up-to-date time in seconds)

Значение для этих переменных указывается в секундах (они определяются в файле `.env`).

Expand Down
42 changes: 42 additions & 0 deletions src/clients/news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Функции для взаимодействия с внешним сервисом-провайдером данных о новостях.
"""
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_NEWS


class NewsClient(BaseClient):
"""
Реализация функций для взаимодействия с внешним сервисом-провайдером данных о новостях.
"""

async def get_base_url(self) -> str:
return "https://newsapi.org/v2/top-headlines"

async def _request(self, endpoint: str) -> Optional[dict]:

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())["articles"]

return None

async def get_news(self, location: str, news_count: int = 3) -> Optional[dict]:
"""
Получение данных о новостях.

:param location: Код страны (например: us, ru, ...)
:param news_count: Максимальное количество новостей
:return:
"""

return await self._request(
f"{await self.get_base_url()}?pageSize={news_count}&page=1&country={location}&apiKey={API_KEY_NEWS}"
)
79 changes: 79 additions & 0 deletions src/collectors/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@
from clients.country import CountryClient
from clients.currency import CurrencyClient
from clients.weather import WeatherClient
from clients.news import NewsClient
from collectors.base import BaseCollector
from collectors.models import (
LocationDTO,
CountryDTO,
CurrencyRatesDTO,
CurrencyInfoDTO,
WeatherInfoDTO,
CountryNewsDTO,
)
from settings import (
MEDIA_PATH,
CACHE_TTL_COUNTRY,
CACHE_TTL_CURRENCY_RATES,
CACHE_TTL_WEATHER,
CACHE_TTL_NEWS,
)


Expand Down Expand Up @@ -91,6 +94,8 @@ async def read(cls) -> Optional[list[CountryDTO]]:
result_list.append(
CountryDTO(
capital=item["capital"],
capital_latitude=item["latitude"],
capital_longitude=item["longitude"],
alpha2code=item["alpha2code"],
alt_spellings=item["alt_spellings"],
currencies={
Expand All @@ -103,6 +108,7 @@ async def read(cls) -> Optional[list[CountryDTO]]:
population=item["population"],
subregion=item["subregion"],
timezones=item["timezones"],
area=item["area"],
)
)

Expand Down Expand Up @@ -219,11 +225,83 @@ async def read(cls, location: LocationDTO) -> Optional[WeatherInfoDTO]:
humidity=result["main"]["humidity"],
wind_speed=result["wind"]["speed"],
description=result["weather"][0]["description"],
visibility=result["visibility"],
timezone=result["timezone"],
dt=result["dt"],
)

return None


class NewsCollector(BaseCollector):
"""
Сбор информации о новостях.
"""

def __init__(self) -> None:
self.client = NewsClient()

@staticmethod
async def get_file_path(filename: str = "", **kwargs: Any) -> str:
return f"{MEDIA_PATH}/news/{filename}.json"

@staticmethod
async def get_cache_ttl() -> int:
return CACHE_TTL_NEWS

async def collect(
self, locations: FrozenSet[LocationDTO] = frozenset(), **kwargs: Any
) -> None:

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.alpha2code}".lower()
if await self.cache_invalid(filename=filename):
# если кэш уже невалиден, то актуализируем его
result = await self.client.get_news(f"{location.alpha2code}")
if result:
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) -> Optional[list[CountryNewsDTO]]:
"""
Чтение данных из кэша.

:param location:
:return:
"""

filename = f"{location.alpha2code}".lower()
async with aiofiles.open(await cls.get_file_path(filename), mode="r") as file:
content = await file.read()

result = json.loads(content)
result_arr = []

if result:
for news_item in result:
result_arr.append(
CountryNewsDTO(
title=news_item["title"],
description=news_item["description"],
url=news_item["url"],
published_at=news_item["publishedAt"],
)
)

return result_arr

return None


class Collectors:
@staticmethod
async def gather() -> tuple:
Expand All @@ -238,6 +316,7 @@ 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:
Expand Down
35 changes: 35 additions & 0 deletions src/collectors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Описание моделей данных (DTO).
"""

from typing import Optional
from pydantic import Field, BaseModel


Expand Down Expand Up @@ -68,6 +69,8 @@ class CountryDTO(BaseModel):

CountryDTO(
capital="Mariehamn",
capital_latitude=60.116667,
capital_longitude=19.9,
alpha2code="AX",
alt_spellings=[
"AX",
Expand All @@ -93,10 +96,13 @@ class CountryDTO(BaseModel):
timezones=[
"UTC+02:00",
],
area=1580
)
"""

capital: str
capital_latitude: float
capital_longitude: float
alpha2code: str
alt_spellings: list[str]
currencies: set[CurrencyInfoDTO]
Expand All @@ -106,6 +112,7 @@ class CountryDTO(BaseModel):
population: int
subregion: str
timezones: list[str]
area: Optional[int]


class CurrencyRatesDTO(BaseModel):
Expand Down Expand Up @@ -140,6 +147,9 @@ class WeatherInfoDTO(BaseModel):
humidity=54,
wind_speed=4.63,
description="scattered clouds",
visibility=10000,
timezone=-21600,
dt=1709996768
)
"""

Expand All @@ -148,6 +158,30 @@ class WeatherInfoDTO(BaseModel):
humidity: int
wind_speed: float
description: str
visibility: int
timezone: int
dt: int


class CountryNewsDTO(BaseModel):
"""
Модель данных о новостях.

.. code-block::

CountryNewsDTO(
title="Fortnite was down all day Friday, but now the 'Myths & Mortals' update is here - The Verge"
description="Fortnite’s Chapter 5 Season 2 launch was unexpectedly delayed for extended server maintenance.
Now v29.00 is live with the “Myths & Mortals” theme."
url="https://www.theverge.com/2024/3/8/24094877/fortnite-down-outage-chapter-5-season-2-update-download""
published_at="2024-03-09T14:14:00Z"
)
"""

title: str
description: str | None
url: str | None
published_at: str | None


class LocationInfoDTO(BaseModel):
Expand Down Expand Up @@ -201,3 +235,4 @@ class LocationInfoDTO(BaseModel):
location: CountryDTO
weather: WeatherInfoDTO
currency_rates: dict[str, float]
news: list[CountryNewsDTO]
18 changes: 17 additions & 1 deletion src/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
"""

from difflib import SequenceMatcher
from typing import Optional
from typing import List, Optional

from collectors.collector import (
CountryCollector,
CurrencyRatesCollector,
NewsCollector,
WeatherCollector,
)
from collectors.models import (
CountryDTO,
CountryNewsDTO,
CurrencyInfoDTO,
LocationDTO,
LocationInfoDTO,
Expand All @@ -38,11 +40,15 @@ async def find(self, location: str) -> Optional[LocationInfoDTO]:
LocationDTO(capital=country.capital, alpha2code=country.alpha2code)
)
currency_rates = await self.get_currency_rates(country.currencies)
news = await self.get_news(
LocationDTO(capital=country.capital, alpha2code=country.alpha2code)
)

return LocationInfoDTO(
location=country,
weather=weather,
currency_rates=currency_rates,
news=news,
)

return None
Expand Down Expand Up @@ -75,6 +81,16 @@ async def get_weather(location: LocationDTO) -> Optional[WeatherInfoDTO]:
"""
return await WeatherCollector.read(location=location)

@staticmethod
async def get_news(location: LocationDTO) -> Optional[List[CountryNewsDTO]]:
"""
Получение данных о новостях.

:param location: Объект локации для получения данных
:return:
"""
return await NewsCollector.read(location=location)

async def find_country(self, search: str) -> Optional[CountryDTO]:
"""
Поиск страны.
Expand Down
Loading