Skip to content

Lab6 #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
101 changes: 98 additions & 3 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,120 @@ GraphQL API Gateway

Зависимости
===========
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).


Установка
=========
Clone the repository to your computer:

.. code-block::shell
git clone https://github.com/mnv/python-course-graphql-gateway
1. To configure the application copy `.env.sample` into `.env` file:

.. code-block::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.

2. Build the container using Docker Compose:

.. code-block::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. To run the project inside the Docker container:

.. code-block::shell
docker compose up
When containers are up server starts at [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql). You can open it in your browser.


Использование
=============

Запросы
_______
This project provides fixtures to test GraphQL. Fixtures are located in src/fixtures. There are JSON-files for favorite places and countries information. GraphQL application uses these fixtures to emulate REST API responses.

Query example to request a list of favorite places:

query {
places {
latitude
longitude
description
city
locality
}
}
Query example to request a list of favorite places with countries information:

query {
places {
latitude
longitude
description
city
locality
country {
name
capital
alpha2code
alpha3code
capital
region
subregion
population
latitude
longitude
demonym
area
numericCode
flag
currencies
languages
}
}
}
This query will request additional information about related countries in optimal way using data loaders to prevent N + 1 requests problem.


Автоматизация
=============
The project contains a special `Makefile` that provides shortcuts for a set of commands:

1. Build the Docker container:

.. code-block::shell
make build
2. Generate Sphinx documentation run:

.. code-block::shell
make docs-html
3. Autoformat source code:

.. code-block::shell
make format
4. Static analysis (linters):

.. code-block::shell
make lint
5. Autotests:

.. code-block::shell
make test
The test coverage report will be located at `src/htmlcov/index.html`.
So you can estimate the quality of automated test coverage.

6. Run autoformat, linters and tests in one command:

Тестирование
============
.. code-block::shell
make all


6 changes: 3 additions & 3 deletions src/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +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]:
"""
Expand All @@ -14,7 +14,7 @@ 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]]:
Expand Down
19 changes: 19 additions & 0 deletions src/dataloaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from promise.dataloader import DataLoader

from services.countries import CountriesService
from services.news import NewsService


class CountryLoader(DataLoader):
Expand All @@ -24,3 +25,21 @@ 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()

return Promise.resolve([news.get(code.lower()) for code in alpha2codes if code])
33 changes: 33 additions & 0 deletions src/fixtures/news/ie.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"status": "ok",
"totalResults": 6036,
"articles":
[
{
"source":
{
"id": "bbc-news",
"name": "BBC News"
},
"author": null,
"title": "Ireland beat Wales to make it three wins in three",
"description": "Ireland move closer to completing back-to-back Six Nations Grand Slams with an unconvincing win over a spirited Wales side in Dublin.",
"url": "https://www.bbc.co.uk/sport/rugby-union/68391672",
"publishedAt": "2024-02-24T16:16:36Z",
"content": "Ciaran Frawley marked his first Six Nations start with an important second-half try\r\n<table><tr><th>Six Nations 2024: Ireland v Wales</th></tr>\r\n<tr><td>Ireland: (17) 31</td></tr><tr><td>Tries: Sheeh\u2026 [+2634 chars]"
},
{
"source":
{
"id": "business-insider",
"name": "Business Insider"
},
"author": "Alexis McSparren",
"title": "I moved from the US to Ireland. Here are 11 things that surprised me most.",
"description": "I spontaneously moved from the US to Ireland, and I wasn't prepared for the differences in the language, the pace of life, or the function of alcohol.",
"url": "https://www.businessinsider.com/surprising-things-moving-to-ireland-from-us-2022-2",
"publishedAt": "2024-02-21T15:17:30Z",
"content": "I've been blown away by the beauty of the Irish countryside.Alexis McSparren\r\n<ul>\n<li>After growing up in the US, I made the spontaneous decision to move to Dublin, Ireland.</li>\n<li>I've experience\u2026 [+10021 chars]"
}
]
}
33 changes: 33 additions & 0 deletions src/fixtures/news/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"status": "ok",
"totalResults": 8746,
"articles":
[
{
"source":
{
"id": "post-news",
"name": "POST News"
},
"author": "https://www.facebook.com/postnews",
"title": "Ticks began to wake up all over Russia. The season of their activity will come in two weeks",
"description": "Precautions should be taken now — with the onset of warm weather, the ticks will finally wake up and switch to nutrition (attacking animals and people).",
"url": "https://www.postnews.ru/news/live/world-europe-68431017",
"publishedAt": "2024-03-29T08:03:55Z",
"content": "The first cases of bites have already been recorded. The number of visits to doctors is growing rapidly in St. Petersburg, Sverdlovsk, Pskov regions and in 19 other regions of the country.\u2026 [+569 chars]"
},
{
"source":
{
"id": null,
"name": "NPR"
},
"author": null,
"title": "Alexei Navalny has died in prison at 47, Moscow says",
"description": "The Federal Prison Service said in a statement that Navalny felt unwell after a walk on Friday and lost consciousness. The politician's team says it has received no confirmation of his death so far.",
"url": "https://www.npr.org/2024/02/16/1231946376/alexei-navalny-death-in-prison-vladimir-putin-opposition",
"publishedAt": "2024-02-16T12:18:22Z",
"content": "Alexei Navalny is seen in 2012 behind the bars in a police van after he was detained during protests in Moscow a day after Putin's inauguration.\r\nSergey Ponomarev/AP\r\nMOSCOW Alexei Navalny, the fierc\u2026 [+1635 chars]"
}
]
}
17 changes: 17 additions & 0 deletions src/models/news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from datetime import datetime

from pydantic import BaseModel, Field


class NewsModel(BaseModel):
"""
Модель для описания новостей.
"""

author: str | None = Field(title="Автор")
source: str = Field(title="Источник")
title: str = Field(title="Заголовок")
description: str | None = Field(title="Описание")
url: str = Field(title="Ссылка")
published_at: datetime = Field(title="Дата публикации")
content: str | None = Field(title="Содержание")
40 changes: 39 additions & 1 deletion src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,25 @@
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()
source = graphene.String()
title = graphene.String()
description = graphene.String()
url = graphene.String()
published_at = graphene.DateTime()
content = graphene.String()


class Country(graphene.ObjectType):
"""
Тип объекта страны.
Expand Down Expand Up @@ -43,6 +57,7 @@ class Place(graphene.ObjectType):
city = graphene.String()
locality = graphene.String()
country = graphene.Field(Country)
news = graphene.List(News)

@staticmethod
def resolve_country(parent: PlaceModel, info: ResolveInfo) -> Promise:
Expand All @@ -61,19 +76,42 @@ 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):
"""
Общий тип для запроса получения данных.
"""

places = graphene.List(Place)
place = graphene.Field(Place, place_id=graphene.Int())

@staticmethod
def resolve_places(
parent: Optional[dict], info: ResolveInfo # pylint: disable=unused-argument
) -> list[PlaceModel]:
return PlacesService().get_places()

@staticmethod
def resolve_place(
parent: Optional[dict], info: ResolveInfo, place_id: int # pylint: disable=unused-argument
) -> list[PlaceModel]:
return PlacesService().get_place(place_id)

schema = Schema(query=Query)
39 changes: 39 additions & 0 deletions src/services/news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json
import os

from models.news import NewsModel


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

@staticmethod
def get_news() -> dict[str, list[NewsModel]]:
"""
Получение списка новостей.
:return:
"""
news_dir = "fixtures/news/"
base_dir = os.path.dirname(os.path.dirname(__file__))
news_dir = os.path.join(base_dir, news_dir)
files = os.listdir(news_dir)
result = {}
for news_file in files:
alpha2code = news_file.split(".")[0]
with open(news_dir + news_file, encoding="utf-8") as file:
if data := json.load(file):
result[alpha2code] = [
NewsModel(
author=article.get("author"),
source=article.get("source").get("name"),
title=article.get("title"),
description=article.get("description"),
url=article.get("url"),
published_at=article.get("publishedAt"),
content=article.get("content"),
)
for article in data.get("articles", [])
]
return result
9 changes: 9 additions & 0 deletions src/services/places.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ def get_places(self) -> list[PlaceModel]:
]

return result

def get_place(self, place_id: int) -> PlaceModel:
"""
Получение любимого места.
:param place_id: Идентификатор.
:return:
"""
result = {place.id: place for place in self.get_places()}
return result.get(place_id, None)