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 @@ -51,3 +51,5 @@ REQUESTS_TIMEOUT=30
CACHE_TTL_CURRENCY_RATES=86_400
# время актуальности данных о погоде (в секундах)
CACHE_TTL_WEATHER=10_700
# время актуальности данных о новостях (в секундах), по умолчанию - час
CACHE_TTL_NEWS=3_600
98 changes: 95 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,114 @@ Service to get up-to-date information about countries and cities.

## Requirements:

Install the appropriate software:

1. [Docker Desktop](https://www.docker.com).
2. [Git](https://github.com/git-guides/install-git).
3. [PyCharm](https://www.jetbrains.com/ru-ru/pycharm/download) (optional).

## Installation
1. To configure the application copy `.env.sample` into `.env` file:
```shell
cp .env.sample .env
```

This file contains environment variables that will share their values across the application.
The sample file (`.env.sample`) contains a set of variables with default values.
So it can be configured depending on the environment.

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 - API for getting news (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
```
This command should be run from the root directory where `Dockerfile` is located.
You also need to build the docker container again in case if you have updated `requirements.txt`.

3. Now it is possible to run the project inside the Docker container:
```shell
docker compose up
```
When containers are up server starts at [http://0.0.0.0:8020](http://0.0.0.0:8020). You can open it in your browser.

4. To run application correctly set up the database using commands:
Connect to the application Docker-container:
```shell
docker compose exec countries-informer-app bash
```
Apply migrations to create tables in the database:
```shell
./manage.py migrate
```

## Usage

1. To get access to Django Admin panel you need to add superuser.
Connect to the application Docker-container (if you are outside the container):
```shell
docker compose exec app bash
```
Create superuser:
```shell
./manage.py createsuperuser
```
2. Go to [http://0.0.0.0:8000/admin](http://0.0.0.0:8000/admin) and manage your jobs and blog posts.

## Usage
## Automation commands

The project contains a special `Makefile` that provides shortcuts for a set of commands:
1. Build the Docker container:
```shell
make build
```

2. Generate Sphinx documentation run:
```shell
make docs-html
```

## Automation commands
3. Autoformat source code:
```shell
make format
```

4. Static analysis (linters):
```shell
make lint
```

5. Autotests:
```shell
make test
```

6. Run autoformat, linters and tests in one command:
```shell
make all
```

Run these commands from the source directory where `Makefile` is located.


## Documentation

The project integrated with the [Sphinx](https://www.sphinx-doc.org/en/master/) documentation engine.
It allows the creation of documentation from source code.
So the source code should contain docstrings in [reStructuredText](https://docutils.sourceforge.io/rst.html) format.

To create HTML documentation run this command from the source directory where `Makefile` is located:
```shell
make docs-html
```


## Contributing
Expand All @@ -28,4 +120,4 @@ Pull requests are welcome. For major changes, please open an issue first to disc
Please make sure to update tests as appropriate.

## License
[MIT](https://choosealicense.com/licenses/mit/)
[MIT](https://choosealicense.com/licenses/mit/)
36 changes: 36 additions & 0 deletions src/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,12 @@
CACHE_TTL_CURRENCY_RATES: int = int(os.getenv("CACHE_TTL_CURRENCY_RATES", "86_400"))
# время актуальности данных о погоде (в секундах), по умолчанию ~ три часа
CACHE_TTL_WEATHER: int = int(os.getenv("CACHE_TTL_WEATHER", "10_700"))
# время актуальности данных о новостях (в секундах), по умолчанию - час
CACHE_TTL_NEWS: int = int(os.getenv("CACHE_TTL_WEATHER", "3_600"))

CACHE_WEATHER = "cache_weather"
CACHE_CURRENCY = "cache_currency"
CACHE_NEWS = "cache_news"
CACHES = {
# общий кэш приложения
"default": {
Expand All @@ -188,6 +191,14 @@
"OPTIONS": {"db": "2"},
"TIMEOUT": CACHE_TTL_CURRENCY_RATES,
},
# кэширование данных о новостях
CACHE_NEWS: {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": BROKER_URL,
"KEY_PREFIX": "news",
"OPTIONS": {"db": "3"},
"TIMEOUT": CACHE_TTL_NEWS,
},
}

# настройки для Celery
Expand Down Expand Up @@ -215,3 +226,28 @@
API_KEY_NEWSAPI = env("API_KEY_NEWSAPI")
# таймаут запросов на внешние ресурсы
REQUESTS_TIMEOUT = env.int("REQUESTS_TIMEOUT")


REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
}

if DEBUG:
INSTALLED_APPS += [
"debug_toolbar",
]
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
INTERNAL_IPS = [
"127.0.0.1",
"localhost",
"172.20.0.6",
]
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": lambda request: True,
}
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
]
1 change: 1 addition & 0 deletions src/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
urlpatterns = [
path("admin/", admin.site.urls),
path("api/v1/", include("geo.urls")),
path("api/v1/", include("news.urls")),
re_path(
r"^swagger(?P<format>\.json|\.yaml)$",
schema_view.without_ui(cache_timeout=0),
Expand Down
41 changes: 41 additions & 0 deletions src/geo/clients/currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Функции для взаимодействия с внешним сервисом-провайдером данных о курсах валют.
"""
from http import HTTPStatus
from typing import Optional

import httpx

from app.settings import REQUESTS_TIMEOUT, API_KEY_APILAYER
from base.clients.base import BaseClient


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

def get_base_url(self) -> str:
return "https://api.apilayer.com/fixer/latest"

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

# формирование заголовков запроса
headers = {"apikey": API_KEY_APILAYER}

with httpx.Client(timeout=REQUESTS_TIMEOUT) as client:
# получение ответа
response = client.get(endpoint, headers=headers)
if response.status_code == HTTPStatus.OK:
return response.json()

return None

def get_rates(self, base: str = "rub") -> Optional[dict]:
"""
Получение данных о курсах валют.
:param base: Базовая валюта
:return: Ответ от API в формате dict
"""

return self._request(f"{self.get_base_url()}?base={base}")
1 change: 1 addition & 0 deletions src/geo/clients/shemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class WeatherInfoDTO(BaseModel):
temp: float
pressure: int
humidity: int
visibility: int
wind_speed: float
description: str

Expand Down
24 changes: 24 additions & 0 deletions src/geo/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Сериализаторы приложения geo"""
from rest_framework import serializers

from geo.models import Country, City
Expand Down Expand Up @@ -47,3 +48,26 @@ class Meta:
"longitude",
"country",
]


class WeatherSerializer(serializers.Serializer):
"""
Сериализатор данных о погоде.
"""

temp = serializers.FloatField()
pressure = serializers.IntegerField()
humidity = serializers.IntegerField()
visibility = serializers.IntegerField()
wind_speed = serializers.FloatField()
description = serializers.CharField()


class CurrencySerializer(serializers.Serializer):
"""
Cериализатор данных о валюте.
"""

base = serializers.CharField()
date = serializers.DateField()
rates = serializers.DictField()
29 changes: 29 additions & 0 deletions src/geo/services/currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Сервисный слой для работы с данными о курсах валют."""
from typing import Optional

from geo.clients.currency import CurrencyClient
from geo.clients.shemas import CurrencyRatesDTO


class CurrencyService:
"""
Сервис для работы с данными о курсах валют.
"""

def get_currency(self, base: str) -> Optional[CurrencyRatesDTO]:
"""
Получение списка курсов валют по её коду.

:param base: Базовая валюта
:return:
"""

data = CurrencyClient().get_rates(base)
if data:
return CurrencyRatesDTO(
base=data["base"],
date=data["date"],
rates=data["rates"],
)

return None
48 changes: 15 additions & 33 deletions src/geo/services/weather.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""Сервисный слой для работы с данными о погоде."""
from typing import Optional

from geo.clients.shemas import CountryDTO
from geo.clients.shemas import WeatherInfoDTO
from geo.clients.weather import WeatherClient
from geo.models import Country


class WeatherService:
"""
Сервис для работы с данными о погоде.
"""

def get_weather(self, alpha2code: str, city: str) -> Optional[dict]:
def get_weather(self, alpha2code: str, city: str) -> Optional[WeatherInfoDTO]:
"""
Получение списка стран по названию.

Expand All @@ -19,33 +19,15 @@ def get_weather(self, alpha2code: str, city: str) -> Optional[dict]:
:return:
"""

if data := WeatherClient().get_weather(f"{city},{alpha2code}"):
return data

return None

def build_model(self, country: CountryDTO) -> Country:
"""
Формирование объекта модели страны.

:param CountryDTO country: Данные о стране.
:return:
"""

return Country(
alpha3code=country.alpha3code,
name=country.name,
alpha2code=country.alpha2code,
capital=country.capital,
region=country.region,
subregion=country.subregion,
population=country.population,
latitude=country.latitude,
longitude=country.longitude,
demonym=country.demonym,
area=country.area,
numeric_code=country.numeric_code,
flag=country.flag,
currencies=[currency.code for currency in country.currencies],
languages=[language.name for language in country.languages],
)
data = WeatherClient().get_weather(f"{city},{alpha2code}")
if data:
return WeatherInfoDTO(
temp=data["main"]["temp"],
pressure=data["main"]["pressure"],
humidity=data["main"]["humidity"],
visibility=data["visibility"],
wind_speed=data["wind"]["speed"],
description=data["weather"][0]["description"],
)

return None
Loading