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
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ LOGGING_LEVEL=DEBUG
API_KEY_APILAYER=
# https://openweathermap.org/price#weather
API_KEY_OPENWEATHER=
# https://newsapi.org/
API_KEY_NEWSAPI=

# время актуальности данных о странах (в секундах)
CACHE_TTL_COUNTRY=31_536_000
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +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 - News Free Plan(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_NEWSAPI` – for NewsAPI access token
2. Build the container using Docker Compose:
```shell
docker compose build
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion cron/crontab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ touch /logs/crontab.log

# обеспечение прав на выполнение файла
chmod a+x /src/collect.py

# добавление правила периодического задания для cron
# * * * * * – выполнение задания один раз в каждую минуту
echo "* * * * * /usr/local/bin/python /src/collect.py >> /logs/crontab.log 2>&1" > /etc/crontab
Expand Down
4 changes: 2 additions & 2 deletions docs/make.bat
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SOURCEDIR=source
set BUILDDIR=build

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
Expand Down
6 changes: 3 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = "Справочник стран"
copyright = f"{date.today().year}, Michael"
author = "Michael"
copyright = f"{date.today().year}, Vladislav"
author = "Vladislav"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand All @@ -32,4 +32,4 @@
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_rtd_theme"
html_theme = "classic"
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 - News Free Plan(https://newsapi.org)

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

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

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` (время актуальности данных о новостях)

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

Expand Down
1 change: 0 additions & 1 deletion src/clients/currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ async def _request(self, endpoint: str) -> Optional[dict]:
async with session.get(endpoint, headers=headers) as response:
if response.status == HTTPStatus.OK:
return await response.json()

return None

async def get_rates(self, base: str = "rub") -> Optional[dict]:
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_NEWSAPI


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

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

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()

return None

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

:param country_code: код страны
:return:
"""

return await self._request(
f"{await self.get_base_url()}/top-headlines?country={country_code}&apiKey={API_KEY_NEWSAPI}"
)
86 changes: 85 additions & 1 deletion src/collectors/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@
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,
CoordInfoDTO,
CurrencyRatesDTO,
CurrencyInfoDTO,
WeatherInfoDTO,
NewsInfoDTO,
)
from settings import (
MEDIA_PATH,
CACHE_TTL_COUNTRY,
CACHE_TTL_CURRENCY_RATES,
CACHE_TTL_WEATHER,
CACHE_TTL_NEWS,
)


Expand Down Expand Up @@ -103,6 +107,7 @@ async def read(cls) -> Optional[list[CountryDTO]]:
population=item["population"],
subregion=item["subregion"],
timezones=item["timezones"],
area=item["area"],
)
)

Expand Down Expand Up @@ -214,16 +219,88 @@ async def read(cls, location: LocationDTO) -> Optional[WeatherInfoDTO]:
result = json.loads(content)
if result:
return WeatherInfoDTO(
coord=CoordInfoDTO(
lon=result["coord"]["lon"],
lat=result["coord"]["lat"],
),
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"],
timezone=result["timezone"],
)

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[NewsInfoDTO]]:
"""
Чтение данных из кэша.

: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)
if result:
return [
NewsInfoDTO(
source=article["source"]["name"],
author=article["author"],
title=article["title"],
description=article["description"],
url=article["url"],
published_at=article["publishedAt"],
)
for article in result["articles"][:3]
]
return None


class Collectors:
@staticmethod
async def gather() -> tuple:
Expand All @@ -232,12 +309,19 @@ async def gather() -> tuple:
CountryCollector().collect(),
)

@staticmethod
async def gather_items(locations: frozenset[LocationDTO]) -> tuple:
return await asyncio.gather(
WeatherCollector().collect(locations),
NewsCollector().collect(locations),
)

@staticmethod
def collect() -> None:
loop = asyncio.get_event_loop()
try:
results = loop.run_until_complete(Collectors.gather())
loop.run_until_complete(WeatherCollector().collect(results[1]))
loop.run_until_complete(Collectors.gather_items(results[1]))
loop.run_until_complete(loop.shutdown_asyncgens())

finally:
Expand Down
Loading