diff --git a/docs/source/index.rst b/docs/source/index.rst
index 45ff020..93c5733 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -6,24 +6,197 @@ GraphQL API Gateway
Зависимости
===========
+Установите требуемое ПО:
+
+1. Docker для контейнеризации – |link_docker|
+
+.. |link_docker| raw:: html
+
+ Docker Desktop
+
+2. Для работы с системой контроля версий – |link_git|
+
+.. |link_git| raw:: html
+
+ Git
+
+3. IDE для работы с исходным кодом – |link_pycharm|
+
+.. |link_pycharm| raw:: html
+
+ PyCharm
Установка
=========
+1. Клонируйте репозиторий проекта в свою рабочую директорию:
+
+ .. code-block:: console
+
+ git clone https://github.com/MNV/python-course-graphql-gateway-example.git
+
+Перед началом использования приложения необходимо его сконфигурировать.
+
+.. note::
+
+ Для конфигурации выполните команды, описанные ниже, находясь в корневой директории проекта (на уровне с директорией `src`).
+
+2. Скопируйте файл настроек `.env.sample`, создав файл `.env`:
+ .. code-block:: console
+
+ cp .env.sample .env
+
+ Этот файл содержит преднастроенные переменные окружения, значения которых будут общими для всего приложения.
+ Файл примера (`.env.sample`) содержит набор переменных со значениями по умолчанию.
+ Созданный файл `.env` можно настроить в зависимости от окружения.
+
+ .. warning::
+
+ Никогда не добавляйте в систему контроля версий заполненный файл `.env` для предотвращения компрометации информации о конфигурации приложения.
+
+3. Соберите Docker-контейнер с помощью Docker Compose:
+ .. code-block:: console
+
+ docker compose build
+
+ Данную команду необходимо выполнять повторно в случае обновления зависимостей в файле `requirements.txt`.
+
+4. После сборки контейнеров можно их запустить командой:
+ .. code-block:: console
+
+ docker compose up
+
+ Данная команда запустит собранные контейнеры для приложения и базы данных.
+ Когда запуск завершится, сервер начнет работать по адресу `http://127.0.0.1:8000`.
+
Использование
=============
-
+Запросы
+-------
+Этот проект предоставляет фикстуры для тестирования GraphQL. Фикстуры расположены в `src/fixtures`.
+В них содержатся JSON-файлы для информации о любимых местах и странах.
+GraphQL-приложение использует эти фикстуры для эмуляции ответов REST API.
+
+Пример запроса на получение списка любимых мест:
+
+.. code-block:: graphql
+
+ query {
+ places {
+ latitude
+ longitude
+ description
+ city
+ locality
+ }
+ }
+
+Пример запроса на получение списка любимых мест с информацией о странах:
+
+.. code-block:: graphql
+
+ query {
+ places {
+ latitude
+ longitude
+ description
+ city
+ locality
+ country {
+ name
+ capital
+ alpha2code
+ alpha3code
+ capital
+ region
+ subregion
+ population
+ latitude
+ longitude
+ demonym
+ area
+ numericCode
+ flag
+ currencies
+ languages
+ }
+ }
+ }
+
+Этот запрос запросит дополнительную информацию о связанных странах оптимальным способом, используя загрузчики данных, чтобы предотвратить проблему N + 1 запросов.
Автоматизация
=============
+Проект содержит специальный файл (`Makefile`) для автоматизации выполнения команд:
+
+1. Сборка Docker-контейнера.
+2. Генерация документации.
+3. Запуск форматирования кода.
+4. Запуск статического анализа кода (выявление ошибок типов и форматирования кода).
+5. Запуск автоматических тестов.
+6. Запуск всех функций поддержки качества кода (форматирование, линтеры, автотесты).
+
+Инструкция по запуску этих команд находится в файле `README.md`.
Тестирование
============
+Для запуска автоматических тестов выполните команду:
+
+.. code-block:: console
+
+ make test
+
+Отчет о тестировании находится в файле `src/htmlcov/index.html`.
+
+
+Документация к исходному коду
+=============================
+
+Сервисный слой
+==============
+
+Сервис для стран
+----------------
+.. automodule:: services.countries
+ :members:
+
+Сервис для новостей
+-------------------
+.. automodule:: services.news
+ :members:
+
+Сервис для мест
+---------------
+.. automodule:: services.places
+ :members:
+
+Модели
+======
+
+Миксин
+------
+.. automodule:: models.mixins
+ :members:
+
+Модель для стран
+----------------
+.. automodule:: models.countries
+ :members:
+
+Модель для новостей
+-------------------
+.. automodule:: models.news
+ :members:
+
+Модель для мест
+---------------
+.. automodule:: models.places
+ :members:
\ No newline at end of file
diff --git a/src/context.py b/src/context.py
index e59ee03..8c6e345 100644
--- a/src/context.py
+++ b/src/context.py
@@ -2,9 +2,10 @@
from promise.dataloader import DataLoader
-from dataloaders import CountryLoader
+from dataloaders import CountryLoader, NewsLoader
DATA_LOADER_COUNTRIES = "countries"
+DATA_LOADER_NEWS = "news"
def register_dataloaders() -> Dict[str, DataLoader]:
@@ -14,7 +15,10 @@ def register_dataloaders() -> Dict[str, DataLoader]:
:return:
"""
- return {DATA_LOADER_COUNTRIES: CountryLoader()}
+ return {
+ DATA_LOADER_COUNTRIES: CountryLoader(),
+ DATA_LOADER_NEWS: NewsLoader(),
+ }
def get_context() -> Dict[str, Dict[str, DataLoader]]:
diff --git a/src/dataloaders.py b/src/dataloaders.py
index 7ea474e..7c3139f 100644
--- a/src/dataloaders.py
+++ b/src/dataloaders.py
@@ -2,6 +2,7 @@
from promise.dataloader import DataLoader
from services.countries import CountriesService
+from services.news import NewsService
class CountryLoader(DataLoader):
@@ -24,3 +25,23 @@ def batch_load_fn( # pylint: disable=method-hidden
# формирование результата с сохранением порядка из alpha2codes
return Promise.resolve([countries_map.get(code) for code in alpha2codes])
+
+
+class NewsLoader(DataLoader):
+ """
+ Загрузчик данных о новостях.
+ """
+
+ def batch_load_fn( # pylint: disable=method-hidden
+ self, alpha2codes: list[str]
+ ) -> Promise:
+ """
+ Функция для загрузки связанных данных по переданному множеству значений.
+ :param alpha2codes: Список ISO Alpha2-кодов стран
+ :return:
+ """
+
+ news = NewsService().get_news()
+
+ # формирование результата с сохранением порядка из alpha2codes
+ return Promise.resolve([news.get(code.lower()) for code in alpha2codes if code])
\ No newline at end of file
diff --git a/src/fixtures/news/ie.json b/src/fixtures/news/ie.json
new file mode 100644
index 0000000..506117b
--- /dev/null
+++ b/src/fixtures/news/ie.json
@@ -0,0 +1,45 @@
+{
+ "status": "ok",
+ "totalResults": 38,
+ "articles": [
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "RTE.ie",
+ "title": "Sunak tells Stormont leaders 'real work starts now' - RTE.ie",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiM2h0dHBzOi8vd3d3LnJ0ZS5pZS9uZXdzLzIwMjQvMDIwNS8xNDMwNTM4LXN0b3Jtb250L9IBH2h0dHBzOi8vYW1wLnJ0ZS5pZS9hbXAvMTQzMDUzOC8?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T14:17:58Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "The Guardian",
+ "title": "Cern aims to build \u20ac20bn atom-smasher to unlock secrets of universe - The Guardian",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMib2h0dHBzOi8vd3d3LnRoZWd1YXJkaWFuLmNvbS9zY2llbmNlLzIwMjQvZmViLzA1L2Nlcm4tYXRvbS1zbWFzaGVyLXVubG9jay1zZWNyZXRzLXVuaXZlcnNlLWxhcmdlLWhhZHJvbi1jb2xsaWRlctIBb2h0dHBzOi8vYW1wLnRoZWd1YXJkaWFuLmNvbS9zY2llbmNlLzIwMjQvZmViLzA1L2Nlcm4tYXRvbS1zbWFzaGVyLXVubG9jay1zZWNyZXRzLXVuaXZlcnNlLWxhcmdlLWhhZHJvbi1jb2xsaWRlcg?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T13:42:00Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "RTE.ie",
+ "title": "Dad's Army star Ian Lavender has died at the age of 77 - RTE.ie",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vd3d3LnJ0ZS5pZS9lbnRlcnRhaW5tZW50LzIwMjQvMDIwNS8xNDMwNTY2LWRhZHMtYXJteS1zdGFyLWlhbi1sYXZlbmRlci1oYXMtZGllZC1hdC10aGUtYWdlLW9mLTc3L9IBAA?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T13:18:45Z",
+ "content": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/fixtures/news/rs.json b/src/fixtures/news/rs.json
new file mode 100644
index 0000000..e60554e
--- /dev/null
+++ b/src/fixtures/news/rs.json
@@ -0,0 +1,45 @@
+{
+ "status": "ok",
+ "totalResults": 34,
+ "articles": [
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "Espreso",
+ "title": "AKO IMATE OVE PROMENE NA KO\u017dI ODMAH SE JAVITE LEKARU: Obi\u010dno se javljaju kod ljudi STARIJIH OD 50 GODINA - Espreso",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vd3d3LmVzcHJlc28uY28ucnMvbGlmZXN0eWxlL3pkcmF2bGplLzE0MjU1MzAvYWtvLWltYXRlLW92ZS1wcm9tZW5lLW5hLWtvemktb2RtYWgtc2UtamF2aXRlLWxla2FyddIBWmh0dHBzOi8vd3d3LmVzcHJlc28uY28ucnMvYW1wLzE0MjU1MzAvYWtvLWltYXRlLW92ZS1wcm9tZW5lLW5hLWtvemktb2RtYWgtc2UtamF2aXRlLWxla2FydQ?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T14:22:12Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "Benchmark",
+ "title": "Apple savitljivi telefon mo\u017eda vidimo izme\u0111u 2026. i 2027. godine - Benchmark",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiY2h0dHBzOi8vYmVuY2htYXJrLnJzL3Zlc3RpL3VyZWRhamkvYXBwbGUtc2F2aXRsaml2aS10ZWxlZm9uLW1vemRhLXZpZGltby1pem1lZHUtMjAyNi1pLTIwMjctZ29kaW5lL9IBAA?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T14:11:45Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "Blic.rs",
+ "title": "\"BLIC\" SAZNAJE: HBO \u0107e Bikovi\u0107u isplatiti ceo honorar za \"White Lotus\", ODU\u0160EVI\u0106E VAS kada budete saznali kome glumac daje ceo iznos - Blic.rs",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMidmh0dHBzOi8vd3d3LmJsaWMucnMvemFiYXZhL2hiby1jZS1iaWtvdmljdS1pc3BsYXRpdGktY2VvLWhvbm9yYXItemEtd2hpdGUtbG90dXMtZXZvLWtvbWUtZ2x1bWFjLWRhamUtY2Vsb2t1cGFuL2NnNjl3ejnSAQA?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T14:08:44Z",
+ "content": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/fixtures/news/ru.json b/src/fixtures/news/ru.json
new file mode 100644
index 0000000..947804f
--- /dev/null
+++ b/src/fixtures/news/ru.json
@@ -0,0 +1,45 @@
+{
+ "status": "ok",
+ "totalResults": 34,
+ "articles": [
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "\u0424\u043e\u043d\u0442\u0430\u043d\u043a\u0430.\u0420\u0443",
+ "title": "\u0417\u0430\u0432\u043e\u0434 \u00ab\u0410\u0432\u0442\u043e\u0442\u043e\u0440\u00bb \u0432 \u041a\u0430\u043b\u0438\u043d\u0438\u043d\u0433\u0440\u0430\u0434\u0435 \u043e\u0431\u044a\u044f\u0432\u0438\u043b \u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0431\u0440\u0435\u043d\u0434\u0430 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 - 5 \u0444\u0435\u0432\u0440\u0430\u043b\u044f 2024 - \u0424\u043e\u043d\u0442\u0430\u043d\u043a\u0430.\u0420\u0443",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiLGh0dHBzOi8vd3d3LmZvbnRhbmthLnJ1LzIwMjQvMDIvMDUvNzMxOTc0NDMv0gEA?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T13:21:21Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "Meduza",
+ "title": "Politico: 13-\u0439 \u043f\u0430\u043a\u0435\u0442 \u0441\u0430\u043d\u043a\u0446\u0438\u0439 \u0415\u0421 \u043f\u0440\u043e\u0442\u0438\u0432 \u0420\u043e\u0441\u0441\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u043d\u043e\u0441\u0438\u0442\u044c \u00ab\u0441\u0438\u043c\u0432\u043e\u043b\u0438\u0447\u0435\u0441\u043a\u0438\u0439\u00bb \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440 \u2014 Meduza - Meduza",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMic2h0dHBzOi8vbWVkdXphLmlvL25ld3MvMjAyNC8wMi8wNS9wb2xpdGljby0xMy15LXBha2V0LXNhbmt0c2l5LWVzLXByb3Rpdi1yb3NzaWktYnVkZXQtbm9zaXQtc2ltdm9saWNoZXNraXktaGFyYWt0ZXLSAQA?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T12:26:00Z",
+ "content": null
+ },
+ {
+ "source": {
+ "id": "google-news",
+ "name": "Google News"
+ },
+ "author": "\u0420\u0411\u041a-\u0423\u043a\u0440\u0430\u0438\u043d\u0430",
+ "title": "\u041d\u0435\u043c\u0435\u0446\u043a\u0438\u0439 \u0433\u0435\u043d\u0435\u0440\u0430\u043b \u0443\u0432\u0435\u0440\u0435\u043d \u0432 \u043f\u043e\u0431\u0435\u0434\u0435 \u0423\u043a\u0440\u0430\u0438\u043d\u044b \u0432 \u0432\u043e\u0439\u043d\u0435 \u0441 \u0420\u043e\u0441\u0441\u0438\u0435\u0439: \u0447\u0442\u043e \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e - \u0420\u0411\u041a-\u0423\u043a\u0440\u0430\u0438\u043d\u0430",
+ "description": null,
+ "url": "https://news.google.com/rss/articles/CBMiWWh0dHBzOi8vd3d3LnJiYy51YS91a3IvbmV3cy9uaW1ldHNraXktZ2VuZXJhbC11cGV2bmVuaXktcGVyZW1vemktdWtyYXlpbmktMTcwNzEzNTQ2OC5odG1s0gFdaHR0cHM6Ly93d3cucmJjLnVhL3Vrci9uZXdzL25pbWV0c2tpeS1nZW5lcmFsLXVwZXZuZW5peS1wZXJlbW96aS11a3JheWluaS0xNzA3MTM1NDY4Lmh0bWwvYW1w?oc=5",
+ "urlToImage": null,
+ "publishedAt": "2024-02-05T12:17:48Z",
+ "content": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/models/news.py b/src/models/news.py
new file mode 100644
index 0000000..733778f
--- /dev/null
+++ b/src/models/news.py
@@ -0,0 +1,16 @@
+from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel, Field
+
+
+class NewsModel(BaseModel):
+ """
+ Модель данных о новостях.
+ """
+
+ author: str = Field(title="Автор")
+ title: str = Field(title="Название")
+ description: Optional[str] = Field(title="Описание")
+ url: str = Field(title="Ссылка")
+ published_at: Optional[datetime] = Field(title="Опубликовано")
\ No newline at end of file
diff --git a/src/schema.py b/src/schema.py
index 7e7c167..2a369f4 100644
--- a/src/schema.py
+++ b/src/schema.py
@@ -5,11 +5,23 @@
from graphql import ResolveInfo
from promise import Promise
-from context import DATA_LOADER_COUNTRIES
+from context import DATA_LOADER_COUNTRIES, DATA_LOADER_NEWS
from models.places import PlaceModel
from services.places import PlacesService
+class News(graphene.ObjectType):
+ """
+ Тип объекта новости.
+ """
+
+ author = graphene.String()
+ title = graphene.String()
+ description = graphene.String()
+ url = graphene.String()
+ published_at = graphene.DateTime()
+
+
class Country(graphene.ObjectType):
"""
Тип объекта страны.
@@ -37,12 +49,14 @@ class Place(graphene.ObjectType):
Тип объекта любимого места.
"""
+ id = graphene.Int()
latitude = graphene.Float()
longitude = graphene.Float()
description = graphene.String()
city = graphene.String()
locality = graphene.String()
country = graphene.Field(Country)
+ news = graphene.List(News)
@staticmethod
def resolve_country(parent: PlaceModel, info: ResolveInfo) -> Promise:
@@ -61,6 +75,22 @@ def resolve_country(parent: PlaceModel, info: ResolveInfo) -> Promise:
return Promise.resolve([])
+ @staticmethod
+ def resolve_news(parent: PlaceModel, info: ResolveInfo) -> Promise:
+ """
+ Получение связанной информации о новостях для объектов любимых мест.
+ :param parent: Объект любимого места.
+ :param info: Объект с метаинформацией и данных о контексте запроса.
+ :return:
+ """
+
+ if info.context:
+ dataloaders = info.context["dataloaders"]
+
+ return dataloaders[DATA_LOADER_NEWS].load(str(parent.country))
+
+ return Promise.resolve([])
+
class Query(graphene.ObjectType):
"""
@@ -68,6 +98,7 @@ class Query(graphene.ObjectType):
"""
places = graphene.List(Place)
+ place = graphene.Field(Place, place_id=graphene.Int())
@staticmethod
def resolve_places(
@@ -75,5 +106,11 @@ def resolve_places(
) -> list[PlaceModel]:
return PlacesService().get_places()
+ @staticmethod
+ def resolve_place(
+ parent: Optional[dict], info: ResolveInfo, place_id: int, # pylint: disable=unused-argument
+ ) -> PlaceModel:
+ return PlacesService().get_place(place_id)
+
schema = Schema(query=Query)
diff --git a/src/services/news.py b/src/services/news.py
new file mode 100644
index 0000000..d0fb85d
--- /dev/null
+++ b/src/services/news.py
@@ -0,0 +1,36 @@
+import json
+import os
+from glob import glob
+
+from models.news import NewsModel
+
+
+class NewsService:
+ """
+ Сервис для работы с новостями.
+ """
+
+ def get_news(self) -> dict[str, list[NewsModel]]:
+ """
+ Получение списка новостей
+ :return:
+ """
+
+ result = {}
+ for filename in glob("fixtures/news/*"):
+ with open(filename, encoding="utf-8") as file:
+ # Получить код из названия файла
+ alpha2code = os.path.basename(filename).split(".")[0]
+ if data := json.load(file):
+ result[alpha2code] = [
+ NewsModel(
+ author=news.get("author"),
+ title=news.get("title"),
+ description=news.get("description"),
+ url=news.get("url"),
+ published_at=news.get("published_at"),
+ )
+ for news in data.get("articles", [])
+ ]
+
+ return result
\ No newline at end of file
diff --git a/src/services/places.py b/src/services/places.py
index ad9672b..d34adff 100644
--- a/src/services/places.py
+++ b/src/services/places.py
@@ -1,5 +1,5 @@
import json
-
+from typing import Optional
from models.places import PlaceModel
@@ -34,3 +34,7 @@ def get_places(self) -> list[PlaceModel]:
]
return result
+
+ def get_place(self, place_id: int) -> Optional[PlaceModel]:
+ result = [place for place in self.get_places() if place.id == place_id]
+ return result[0] if len(result) > 0 else None
\ No newline at end of file
diff --git a/src/tests/functional/test_context.py b/src/tests/functional/test_context.py
new file mode 100644
index 0000000..0b8d7be
--- /dev/null
+++ b/src/tests/functional/test_context.py
@@ -0,0 +1,33 @@
+from context import get_context, register_dataloaders
+from dataloaders import CountryLoader, NewsLoader
+
+
+def test_get_context():
+ """
+ Тестирование контекста
+ """
+ context = get_context()
+
+ assert len(context) == 1
+ assert context.keys() == {"dataloaders"}
+ assert isinstance(context["dataloaders"], dict)
+ assert len(context["dataloaders"]) == 2
+
+ dataloaders = context["dataloaders"]
+ assert dataloaders.keys() == {"countries", "news"}
+ assert isinstance(dataloaders["countries"], CountryLoader)
+ assert isinstance(dataloaders["news"], NewsLoader)
+
+
+def test_register_dataloaders():
+ """
+ Тестирование содержимого контекста
+ """
+ dataloaders = register_dataloaders()
+
+ assert len(dataloaders) == 2
+ assert isinstance(dataloaders, dict)
+ assert dataloaders.keys() == {"countries", "news"}
+
+ assert isinstance(dataloaders["countries"], CountryLoader)
+ assert isinstance(dataloaders["news"], NewsLoader)
\ No newline at end of file
diff --git a/src/tests/functional/test_graphql.py b/src/tests/functional/test_graphql.py
new file mode 100644
index 0000000..33061af
--- /dev/null
+++ b/src/tests/functional/test_graphql.py
@@ -0,0 +1,132 @@
+import pytest
+from graphene.test import Client
+
+from context import get_context
+from schema import schema
+
+
+@pytest.fixture
+def context_fixture():
+ yield get_context()
+
+
+def test_get_places(context_fixture):
+ query = """
+ {
+ places {
+ latitude
+ longitude
+ description
+ city
+ locality
+ }
+ }
+ """
+ client = Client(schema, context_value=context_fixture)
+ executed = client.execute(query)
+ assert isinstance(executed, dict)
+
+ places = executed["data"]["places"]
+ assert len(places) == 6
+
+ place = places[0]
+ assert place["latitude"] == 58.0081
+ assert place["longitude"] == 56.249
+ assert place["description"] == "Супер место!"
+ assert place["city"] == "Perm"
+ assert place["locality"] == "Sverdlovsky City District"
+
+
+def test_places_with_country_and_news(context_fixture):
+ query = """
+ {
+ places {
+ description
+ city
+ country {
+ name
+ alpha2code
+ capital
+ }
+ news {
+ title
+ }
+ }
+ }
+ """
+ client = Client(schema, context_value=context_fixture)
+ executed = client.execute(query)
+ assert isinstance(executed, dict)
+
+ places = executed["data"]["places"]
+ assert len(places) == 6
+
+ place = places[0]
+ # проверка, что не пришло то, что не заказывали
+ assert place.get("latitude", None) is None
+ assert place.get("longitude", None) is None
+ assert place["description"] == "Супер место!"
+ assert place["city"] == "Perm"
+
+ country = place["country"]
+ assert country["name"] == "Russian Federation"
+ assert country["capital"] == "Moscow"
+ assert country["alpha2code"] == "RU"
+
+
+def test_place_with_country_and_news(context_fixture):
+ query = """
+ {
+ place(placeId: 57) {
+ description
+ city
+ locality
+ country {
+ name
+ capital
+ alpha2code
+ }
+ news {
+ author
+ title
+ description
+ url
+ publishedAt
+ }
+ }
+ }
+ """
+
+ client = Client(schema, context_value=context_fixture)
+ executed = client.execute(query)
+ assert isinstance(executed, dict)
+
+ place = executed["data"]["place"]
+ assert len(place) == 5
+
+ assert place["description"] == "Графтон-стрит."
+ assert place["city"] == "Dublin"
+ assert place["locality"] == "Portobello"
+ # проверка, что не пришло то, что не заказывали
+ assert place.get("latitude", None) is None
+ assert place.get("longitude", None) is None
+
+ country = place["country"]
+ assert country["name"] == "Ireland"
+ assert country["capital"] == "Dublin"
+ assert country["alpha2code"] == "IE"
+
+ news = place["news"]
+ assert len(news) == 3
+ assert (
+ news[0]["title"]
+ == "Sunak tells Stormont leaders 'real work starts now' - RTE.ie"
+ )
+ assert (
+ news[1]["title"]
+ == "Cern aims to build €20bn atom-smasher to unlock secrets of universe - The Guardian"
+ )
+ assert (
+ news[2]["title"]
+ == "Dad's Army star Ian Lavender has died at the age of 77 - RTE.ie"
+ )
\ No newline at end of file