{"id":398359,"date":"2024-06-29T14:01:45","date_gmt":"2024-06-29T14:01:45","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=398359"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-06-29T14:01:45","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=398359","title":{"rendered":"<span>\u0413\u0430\u0439\u0434 \u043f\u043e \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e Python-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 GraphQL-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 Strawberry<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/245\/951\/b9f\/245951b9f7d4f89cc436243465e381e5.jpg\" width=\"512\" height=\"512\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/245\/951\/b9f\/245951b9f7d4f89cc436243465e381e5.jpg\" data-blurred=\"true\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041f\u0438\u0448\u0435\u0442\u0435 \u043d\u0430 Python \u0438 \u0434\u0430\u0432\u043d\u043e \u0445\u043e\u0442\u0435\u043b\u0438 \u0437\u0430\u043f\u0440\u044b\u0433\u043d\u0443\u0442\u044c \u043d\u0430 \u043f\u043e\u0435\u0437\u0434 \u0445\u0430\u0439\u043f\u0430 \u043f\u043e <a href=\"https:\/\/graphql.org\/\" rel=\"noopener noreferrer nofollow\"><u>GraphQL<\/u><\/a>, \u043d\u043e \u043d\u0438\u043a\u0430\u043a \u043d\u0435 \u043c\u043e\u0433\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u043c\u0435\u0436\u0434\u0443 <a href=\"https:\/\/graphene-python.org\/\" rel=\"noopener noreferrer nofollow\"><u>Graphene<\/u><\/a> \u0438 <a href=\"https:\/\/ariadnegraphql.org\/\" rel=\"noopener noreferrer nofollow\"><u>Ariadne<\/u><\/a>? \u041f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u0442\u0440\u0435\u0442\u0438\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2013 <a href=\"https:\/\/strawberry.rocks\/\" rel=\"noopener noreferrer nofollow\"><u>Strawberry<\/u><\/a>.<\/p>\n<p><a href=\"https:\/\/strawberry.rocks\/\" rel=\"noopener noreferrer nofollow\">Strawberry<\/a> \u2013 code-first \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441 \u0431\u043e\u043b\u044c\u0448\u0438\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u0431\u0430\u0442\u0430\u0440\u0435\u0435\u043a. 2.6 \u0442\u044b\u0441. \u0437\u0432\u0451\u0437\u0434 \u0432 <a href=\"https:\/\/github.com\/strawberry-graphql\/strawberry\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u043d\u0430 GitHub<\/u><\/a>. \u0414\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u043e\u0432 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c dataclasses \u0438 pydantic-\u043c\u043e\u0434\u0435\u043b\u0438. \u0418\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0441\u0442\u044c.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u043c \u0433\u0430\u0439\u0434\u0435 \u043c\u044b \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0435\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 (users) \u0438 \u0438\u0445 \u043a\u043d\u0438\u0433 (books).<\/p>\n<ul>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f (user).<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043d\u0438\u0433\u0443 (book) \u0441\u043e \u0441\u0441\u044b\u043b\u043a\u043e\u0439 \u043d\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043d\u0438\u0433\u0438 \u0441\u043e \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u043c\u0438 \u0432 \u043d\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0425\u0440\u0430\u043d\u0438\u0442\u044c \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0431\u0443\u0434\u0435\u043c \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u0437\u043d\u0430\u0447\u0438\u0442, \u043d\u0430\u0448\u0438\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043c \u0431\u0443\u0434\u0435\u0442 IO-bound. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u0443\u0434\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043e\u0434.<\/p>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f:<\/p>\n<ul>\n<li>\n<p>\u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 Strawberry \u0441 \u0434\u0432\u0438\u0436\u043a\u043e\u043c <a href=\"https:\/\/fastapi.tiangolo.com\/\" rel=\"noopener noreferrer nofollow\"><u>fastAPI<\/u><\/a> \u043d\u0430 Python 3.10;<\/p>\n<\/li>\n<li>\n<p>\u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0430\u0437\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c postgreSQL \u0432\u0435\u0440\u0441\u0438\u0438 14 \u0441 python-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 <a href=\"https:\/\/www.encode.io\/databases\/\" rel=\"noopener noreferrer nofollow\">encode\/databases<\/a> \u0438 \u0434\u0432\u0438\u0436\u043a\u043e\u043c <a href=\"https:\/\/magicstack.github.io\/asyncpg\/current\/\" rel=\"noopener noreferrer nofollow\">asyncpg<\/a>;<\/p>\n<\/li>\n<li>\n<p>\u0434\u043b\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f <a href=\"https:\/\/pydantic-docs.helpmanual.io\/\" rel=\"noopener noreferrer nofollow\">pydantic<\/a>;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0441 \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u043e\u0439 <a href=\"https:\/\/relay.dev\/graphql\/connections.htm\" rel=\"noopener noreferrer nofollow\">relay-style<\/a> \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0435\u0439 \u0438 <a href=\"https:\/\/github.com\/graphql\/dataloader\" rel=\"noopener noreferrer nofollow\">dataloader<\/a>&#8216;\u0430\u043c\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0443\u0442\u0430\u0446\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0441 <a href=\"https:\/\/docs.pytest.org\/en\/7.1.x\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a>.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0434\u0440\u0430\u0437\u0443\u043c\u0435\u0432\u0430\u0435\u043c, \u0447\u0442\u043e \u0432\u044b \u0443\u0436\u0435 \u0437\u043d\u0430\u043a\u043e\u043c\u044b \u0441 \u043a\u043e\u043d\u0446\u0435\u043f\u0446\u0438\u044f\u043c\u0438 GraphQL. \u0418\u0437\u0443\u0447\u0438\u0442\u044c \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430 <a href=\"https:\/\/graphql.org\/learn\/\" rel=\"noopener noreferrer nofollow\"><u>\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0440\u0442\u0430\u043b\u0435 GraphQL<\/u><\/a>.<\/p>\n<h3>\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/h3>\n<p>\u0414\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u0430\u044f \u0421\u0423\u0411\u0414 postgreSQL \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0430\u044f \u0432 \u043d\u0435\u0439 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<pre><code class=\"bash\">docker run -d --restart=always -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres --name postgres postgres:14.3-alpine  psql \"postgresql:\/\/postgres:postgres@localhost:5432\" -\u0441 'create database strawberry;'<\/code><\/pre>\n<h3>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438<\/h3>\n<p>\u041f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u043b\u043e\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 <a href=\"https:\/\/python-poetry.org\/\" rel=\"noopener noreferrer nofollow\"><u>Poetry<\/u><\/a>. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0435\u0433\u043e:<\/p>\n<pre><code class=\"bash\">pip install poetry<\/code><\/pre>\n<p>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.<\/p>\n<pre><code>poetry init<\/code><\/pre>\n<p>\u0412\u0441\u0451 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043a\u0440\u043e\u043c\u0435 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 \u043f\u0440\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 (dependencies). \u0422\u0430\u043c \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u043c no.<\/p>\n<p>\u0421\u0442\u0430\u0432\u0438\u043c \u043f\u0430\u043a\u0435\u0442\u044b:<\/p>\n<ul>\n<li>\n<p>Strawberry \u2013 \u0441\u0430\u043c\u0430 GraphQL-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0432\u0435\u0440\u0441\u0438\u044e \u0441 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \u0441 fastAPI;<\/p>\n<\/li>\n<li>\n<p>Databases \u2013 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u0430\u043c\u0438. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0432\u0435\u0440\u0441\u0438\u044e \u0441 \u0434\u0440\u0430\u0439\u0432\u0435\u0440\u043e\u043c asyncpg;<\/p>\n<\/li>\n<li>\n<p>Uvicorn \u2013 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f;<\/p>\n<\/li>\n<li>\n<p>Yoyo-migrations \u2013 \u0443\u0442\u0438\u043b\u0438\u0442\u0430 \u0434\u043b\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p>Psycopg2-binary \u2013 \u0434\u0432\u0438\u0436\u043e\u043a, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p>Pypika \u2013 \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 SQL query builder.<\/p>\n<\/li>\n<\/ul>\n<p>\u0418 \u043f\u0430\u043a\u0435\u0442\u044b \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438:<\/p>\n<ul>\n<li>\n<p>pytest \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>pytest-asyncio \u0434\u043b\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>pytest-mock \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 mock;<\/p>\n<\/li>\n<li>\n<p>mypy \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0442\u0438\u043f\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>httpx \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043c\u0438.<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"bash\">poetry add 'strawberry-graphql[fastapi]@0.128.0' 'databases[asyncpg]' 'uvicorn[standard]' yoyo-migrations psycopg2-binary pypika  poetry add -D pytest pytest-asyncio pytest-mock mypy httpx<\/code><\/pre>\n<h3>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0440\u0430\u0437\u0443 \u0432\u0441\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<pre><code class=\"bash\">mkdir migrations touch migrations\/000001.init.sql touch migrations\/000001.init.rollback.sql touch run.py touch settings.py mkdir -p src\/users mkdir -p src\/books touch src\/__init__.py touch src\/context.py touch src\/users\/__init__.py touch src\/users\/gql.py touch src\/users\/models.py touch src\/books\/__init__.py touch src\/books\/gql.py touch src\/books\/models.py mkdir -p tests\/fixtures touch tests\/__init__.py touch tests\/conftest.py touch tests\/test_users.py touch tests\/test_books.py touch tests\/fixtures\/__init__.py touch tests\/fixtures\/clients.py touch tests\/fixtures\/graphql_client.py touch .env touch mypy.ini<\/code><\/pre>\n<h3>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435<\/h3>\n<p>\u0412 .env \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0430\u0437\u0435:<\/p>\n<pre><code>PORT=8002 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB_NAME=postgres<\/code><\/pre>\n<p>\u0411\u0443\u0434\u0435\u043c \u0447\u0438\u0442\u0430\u0442\u044c \u0438\u0437 .env \u0432 settings.py:<\/p>\n<pre><code class=\"python\">import os  PORT = int(os.environ.get('PORT', 8001)) DB_USER = os.environ['POSTGRES_USER'] DB_PASSWORD = os.environ['POSTGRES_PASSWORD'] DB_SERVER = os.environ['POSTGRES_HOST'] DB_PORT = int(os.environ['POSTGRES_PORT']) DB_NAME = os.environ['POSTGRES_DB_NAME'] CONN_TEMPLATE = (     'postgresql+asyncpg:\/\/{user}:{password}@{host}:{port}\/{name}' ) MIGRATIONS_CONN_TEMPLATE = (     'postgresql:\/\/{user}:{password}@{host}:{port}\/{name}' ) DEFAULT_LIMIT = 100<\/code><\/pre>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043a \u0431\u0430\u0437\u0435 \u0432 run.py:<\/p>\n<pre><code class=\"python\">import functools as fn import typing as tp  import databases import settings import strawberry import uvicorn from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig   @strawberry.type class Query():     \"\"\"Query.\"\"\"      @strawberry.field     def hello(self) -> str:         return 'world'   schema = strawberry.Schema(     query=Query,     config=StrawberryConfig(auto_camel_case=True), )   async def startup_db(db: databases.Database):     await db.connect()   async def shutdown_db(db: databases.Database):     await db.disconnect()   HOOK_TYPE = tp.Optional[     tp.Sequence[tp.Callable[[], tp.Any]] ]   def get_app(     db: databases.Database,     on_startup: HOOK_TYPE = None,     on_shutdown: HOOK_TYPE = None, ) -> FastAPI:     app = FastAPI(         on_startup=[fn.partial(startup_db, db)]         if on_startup is None else on_startup,         on_shutdown=[fn.partial(shutdown_db, db)]         if on_shutdown is None else on_shutdown,     )     graphql_app = GraphQLRouter(         schema,     )     app.include_router(graphql_app, prefix='\/graphql')     return app   def main() -> None:     database = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=settings.DB_NAME,         ),     )     app = get_app(database)     uvicorn.run(         app,         host='0.0.0.0',         port=settings.PORT,     )   if __name__ == '__main__':     main() <\/code><\/pre>\n<p>\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u043c:<\/p>\n<pre><code class=\"bash\">poetry run python3 run.py<\/code><\/pre>\n<p>\u0418 \u043e\u0442\u043a\u0440\u043e\u0435\u043c \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435: <a href=\"http:\/\/localhost:8002\/graphql\" rel=\"noopener noreferrer nofollow\">http:\/\/localhost:8002\/graphql<\/a>.<\/p>\n<p>\u041e\u0442\u043a\u0440\u043e\u0435\u0442\u0441\u044f GraphiQL Playground. \u0412 \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0434\u0451\u0440\u043d\u0435\u043c \u043d\u0430\u0448\u0443 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e \u0440\u0443\u0447\u043a\u0443:<\/p>\n<pre><code class=\"json\">{   hello }<\/code><\/pre>\n<p>\u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043c:<\/p>\n<pre><code class=\"json\">{   \"data\": {     \"hello\": \"world\"   } }<\/code><\/pre>\n<h3>\u041c\u043e\u0434\u0435\u043b\u0438<\/h3>\n<p>\u041c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u2013 migrations\/000001.init.sql:<\/p>\n<pre><code class=\"sql\">create table users (     id bigserial primary key,     name text not null ); create table books (     id bigserial primary key,     user_id bigint not null references users(id) on delete cascade,     title text not null );<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u043d\u0430\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u2013 migrations\/000001.init.rollback.sql:<\/p>\n<pre><code class=\"sql\">drop table books; drop table users;<\/code><\/pre>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u043c\u043e\u0434\u0435\u043b\u0438, \u0438\u0445 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435.<\/p>\n<p>\u0414\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f pydantic \u043c\u043e\u0434\u0435\u043b\u044f\u043c\u0438.<\/p>\n<p>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u2013 src\/users\/models.py:<\/p>\n<pre><code class=\"python\">from typing import Optional from databases import Database import pydantic from pypika.dialects import PostgreSQLQuery as Query, Table   class User(pydantic.BaseModel):     id: int     name: str   class CreateUserInput(pydantic.BaseModel):     name: str   async def get_users(     db: Database,     ids: Optional[list[int]] = None, ) -> list[User]:     users_tb = Table('users')     query = Query.from_(users_tb).select(users_tb.star)      if ids is not None:         if not ids:             return []         query = query.where(             users_tb.field('id').isin(ids),         )      return [         User(**el._mapping)         for el in await db.fetch_all(query=str(query))     ]   async def create_user(     db: Database,     create_input: CreateUserInput, ) -> User:     users_tb = Table('users')     query = Query.into(users_tb).columns(         tuple(create_input.dict().keys()),     ).returning(users_tb.star)     for value in create_input.dict().values():         query = query.insert(value)     row = await db.fetch_one(query=str(query))     if not row:         raise ValueError('empty row returned')     return User(**row._mapping)  \u041a\u043d\u0438\u0433\u0438 \u2013 src\/books\/models.py: from typing import Optional from databases import Database import pydantic from pypika.dialects import PostgreSQLQuery as Query, Table   class Book(pydantic.BaseModel):     id: int     title: str     user_id: int   class CreateBookInput(pydantic.BaseModel):     title: str     user_id: int   async def get_books(     db: Database,     ids: Optional[list[int]] = None,     after: str | None = None,     first: int | None = None, ) -> list[Book]:     books_tb = Table('books')     query = Query.from_(books_tb).select(books_tb.star)      if ids is not None:         if not ids:             return []         query = query.where(             books_tb.field('id').isin(ids),         )     if after:         query = query.where(             books_tb.field('id').gt(int(after)),         )     if first:         query = query.limit(first)      return [         Book(**el._mapping)         for el in await db.fetch_all(query=str(query))     ]   async def create_book(     db: Database,     create_input: CreateBookInput, ) -> Book:     books_tb = Table('books')     query = Query.into(books_tb).columns(         tuple(create_input.dict().keys()),     ).returning(books_tb.star).insert(         list(create_input.dict().values()),     )     row = await db.fetch_one(query=str(query))     if not row:         raise ValueError('empty row returned')     return Book(**row._mapping)<\/code><\/pre>\n<p>\u0412 get_books \u043c\u044b \u0437\u0430\u0440\u0430\u043d\u0435\u0435 \u043e\u043f\u0438\u0441\u0430\u043b\u0438, \u043f\u043e\u043c\u0438\u043c\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430 \u043f\u043e \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a\u0430\u043c, \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u0443\u044e \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044e:<\/p>\n<ul>\n<li>\n<p>first \u2013 \u0430\u043d\u0430\u043b\u043e\u0433 limit \u0432 limit-offset&#8217;\u043d\u043e\u0439 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>after \u2013 \u043a\u0443\u0440\u0441\u043e\u0440. \u0417\u0434\u0435\u0441\u044c \u0431\u0443\u0434\u0435\u043c \u0441\u0447\u0438\u0442\u0430\u0442\u044c, \u0447\u0442\u043e \u043a\u0443\u0440\u0441\u043e\u0440 \u2013 \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0422\u0438\u043f\u044b \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/h3>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u0442\u0438\u043f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u2013 UserType.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0435\u0439 pydantic-\u0442\u0438\u043f\u043e\u0432 \u0432 \u0442\u0438\u043f\u044b strawberry. strawberry.auto \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u043c\u044b:<\/p>\n<ul>\n<li>\n<p>\u043a\u043e\u043f\u0438\u0440\u0443\u0435\u043c \u043f\u043e\u043b\u044f \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0438, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 model \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 type;<\/p>\n<\/li>\n<li>\n<p>\u0442\u0438\u043f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438;<\/p>\n<\/li>\n<li>\n<p>GraphQL-\u0438\u043c\u044f \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0432 \u043a\u043e\u043d\u0432\u0435\u043d\u0446\u0438\u0438 camelCase.<\/p>\n<\/li>\n<\/ul>\n<p>\u0421\u0440\u0430\u0437\u0443 \u0436\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u044e\u0437\u0435\u0440\u043e\u0432. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043e\u043f\u0438\u0448\u0435\u043c \u0442\u0438\u043f UserQuery. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 extend=True \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u0432 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0435 \u043d\u0430 \u044f\u0437\u044b\u043a GraphQL \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f extend type Query.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u043c \u0442\u0438\u043f\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u043c resolver \u0441 \u0438\u043c\u0435\u043d\u0435\u043c user. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e user \u0438 \u043f\u043e\u0432\u0435\u0441\u0438\u0442\u044c \u043d\u0430 \u043d\u0435\u0451 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 field. \u041e\u043d \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 id \u0442\u0438\u043f\u0430 int \u2013 \u044d\u0442\u043e \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>Info[Context, None] \u2013 \u044d\u0442\u043e \u0432\u043d\u0443\u0442\u0440\u044f\u043d\u043a\u0430 Strawberry. \u041f\u0440\u043e \u043d\u0435\u0433\u043e \u0438 \u043f\u0440\u043e user_loader \u2013 \u043f\u043e\u0437\u0436\u0435.<\/p>\n<p>\u0410\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0442\u0438\u043f\u0443 \u044e\u0437\u0435\u0440\u0430 \u043c\u044b \u043e\u043f\u0438\u0448\u0435\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f. \u041e\u0442\u043b\u0438\u0447\u0438\u0435 \u2013 \u0432\u043c\u0435\u0441\u0442\u043e type \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 input.<\/p>\n<p>\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0431\u044b\u043b\u043e \u043d\u0443\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u043c\u0443\u0442\u0430\u0446\u0438\u044e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f. \u041e\u0447\u0435\u043d\u044c \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 resolver&#8217;\u0430 user, \u043d\u043e \u0441 \u043e\u0442\u043b\u0438\u0447\u0438\u0435\u043c, \u0447\u0442\u043e \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 \u0442\u0435\u043f\u0435\u0440\u044c \u2013 mutation.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043e\u0431\u0451\u0440\u0442\u043a\u0443 Annotated. \u0417\u0434\u0435\u0441\u044c \u043e\u043d\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043c\u0443\u0442\u0430\u0446\u0438\u0438 \u043d\u0430 input \u0432\u043c\u0435\u0441\u0442\u043e \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 create_input_gql \u0432 createInputGql.<\/p>\n<p>\u0415\u0449\u0451 \u043e\u0434\u0438\u043d \u0432\u0430\u0436\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432 \u043c\u0443\u0442\u0430\u0446\u0438\u0438 \u2013 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0438\u043f\u0430 strawberry \u0432 pydantic \u043f\u0440\u0438 \u043f\u043e\u043c\u043e\u0449\u0438 create_input = create_input_gql.to_pydantic(). \u042d\u0442\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u044f\u0435\u0442 pydantic \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u0438 \u00ab\u0443\u0441\u043f\u043e\u043a\u0430\u0438\u0432\u0430\u0435\u0442\u00bb mypy. \ud83d\ude42<\/p>\n<p>\u041f\u043e\u043b\u0443\u0447\u0438\u0432\u0448\u0438\u0439\u0441\u044f \u043a\u043e\u0434 \u0432 src\/users\/gql.py:<\/p>\n<pre><code class=\"python\">from typing import Annotated, cast import strawberry from src.context import Context from src.users.models import (     CreateUserInput,     User,     create_user, ) from strawberry.types import Info    @strawberry.experimental.pydantic.type(model=User, name='User') class UserType():     id: strawberry.auto     name: strawberry.auto   @strawberry.type(name='Query', extend=True) class UserQuery:      @strawberry.field(name='user')     async def user(         self,         info: Info[Context, None],         id: int,     ) -> UserType | None:         return await info.context.user_loader.load(id)   @strawberry.experimental.pydantic.input(     model=CreateUserInput, name='CreateUserInput', ) class CreateUserInputType:     name: strawberry.auto   @strawberry.type(name='Mutation', extend=True) class UserMutation:      @strawberry.mutation()     async def create_user(         self,         info: Info[Context, None],         create_input_gql: Annotated[             CreateUserInputType,             strawberry.argument(name='input'),         ],     ) -> UserType:         create_input = create_input_gql.to_pydantic()         return cast(             UserType,             await create_user(                 db=info.context.db,                 create_input=create_input,             ),         ) <\/code><\/pre>\n<p>\u0412\u0435\u0440\u043d\u0451\u043c\u0441\u044f \u043a \u043e\u0431\u0435\u0449\u0430\u043d\u043d\u043e\u043c\u0443 Info, Context \u0438 user_loader, \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u043c \u0444\u0430\u0439\u043b src\/context.py.<\/p>\n<p>Context \u043c\u043e\u0436\u043d\u043e \u0432\u043e\u0441\u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0435\u0448\u043e\u043a \u0441 \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u043c\u0438 \u0432\u0435\u0449\u0430\u043c\u0438 \u2013 \u0432\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439. \u0412 GraphQL \u0442\u0443\u0434\u0430 \u043d\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u044e\u0442\u0441\u044f dataloader&#8217;\u044b. \u0422\u0443\u0434\u0430 \u0438\u0445 \u0438 \u043f\u043e\u043b\u043e\u0436\u0438\u043c, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0430\u0437\u0435.<\/p>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c dataloader \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0430\u043f\u0438\u0448\u0435\u043c partial-\u0444\u0443\u043d\u043a\u0446\u0438\u044e. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043d\u0430\u043c \u0432 \u043d\u0430\u0447\u0430\u043b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043f\u043e\u0434\u0441\u0443\u043d\u0443\u0442\u044c \u0432 \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0430\u0437\u0435. \u0422\u0430\u043a, \u043f\u0440\u0438 \u0432\u044b\u0437\u043e\u0432\u0435 load-\u0444\u0443\u043d\u043a\u0446\u0438\u0438 dataloader&#8217;\u0430, \u043d\u0430\u043c \u043e\u0441\u0442\u0430\u043d\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0441\u0443\u043d\u0443\u0442\u044c \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u0412 src\/context.py \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c:<\/p>\n<pre><code class=\"python\">from functools import partial from typing import TYPE_CHECKING, Optional import databases from strawberry.dataloader import DataLoader from strawberry.fastapi import BaseContext  from src.users.models import get_users  if TYPE_CHECKING:     from src.users.gql import UserType   class Context(BaseContext):     \"\"\"Context.\"\"\"      db: databases.Database      def __init__(         self,         db: databases.Database,         user_loader: DataLoader[int, Optional['UserType']],     ):         self.db = db         self.user_loader = user_loader   def get_context(db: databases.Database) -> Context:     return Context(         db=db,         user_loader=DataLoader(             load_fn=partial(get_users, db),         )     ) <\/code><\/pre>\n<p>\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0437\u043d\u0430\u043b\u043e \u043e \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 (\u0438 \u043d\u0430\u0447\u0430\u043b\u043e \u0435\u0433\u043e \u0437\u0430\u043f\u043e\u043b\u043d\u044f\u0442\u044c), \u0430 \u0442\u0430\u043a\u0436\u0435 \u043c\u0443\u0442\u0430\u0446\u0438\u0438 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u044e\u0437\u0435\u0440\u0430, \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c \u043d\u0430\u0448 run.py.<\/p>\n<p>\u0412 graphql_app \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f context_getter:<\/p>\n<pre><code>graphql_app = GraphQLRouter(     schema,     context_getter=fn.partial(get_context, db), )<\/code><\/pre>\n<p>\u0418\u0437 Query \u043f\u0440\u043e\u043f\u0430\u0434\u0451\u0442 hello, \u043d\u043e \u043e\u0442\u043d\u0430\u0441\u043b\u0435\u0434\u0443\u0435\u0442\u0441\u044f UserQuery:<\/p>\n<pre><code class=\"python\">@strawberry.type class Query(     UserQuery, ):     \"\"\"Query.\"\"\"<\/code><\/pre>\n<p>\u041f\u043e\u044f\u0432\u044f\u0442\u0441\u044f \u043c\u0443\u0442\u0430\u0446\u0438\u0438:<\/p>\n<pre><code class=\"python\">@strawberry.type class Mutation(     UserMutation, ):     \"\"\"Mutations.\"\"\"<\/code><\/pre>\n<p>\u041c\u0443\u0442\u0430\u0446\u0438\u0438 \u043d\u0430\u0434\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0432 \u0441\u0445\u0435\u043c\u0435:<\/p>\n<pre><code class=\"python\">schema = strawberry.Schema(     query=Query,     mutation=Mutation,     config=StrawberryConfig(auto_camel_case=True), )<\/code><\/pre>\n<p>\u0418\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0444\u0430\u0439\u043b:<\/p>\n<pre><code class=\"python\">import functools as fn import typing as tp  import databases import settings import strawberry import uvicorn from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig  from src.context import get_context from src.users.gql import UserMutation, UserQuery   @strawberry.type class Query(     UserQuery, ):     \"\"\"Query.\"\"\"   @strawberry.type class Mutation(     UserMutation, ):     \"\"\"Mutations.\"\"\"   schema = strawberry.Schema(     query=Query,     mutation=Mutation,     config=StrawberryConfig(auto_camel_case=True), )   async def startup_db(db: databases.Database):     await db.connect()   async def shutdown_db(db: databases.Database):     await db.disconnect()   HOOK_TYPE = tp.Optional[     tp.Sequence[tp.Callable[[], tp.Any]] ]   def get_app(     db: databases.Database,     on_startup: HOOK_TYPE = None,     on_shutdown: HOOK_TYPE = None, ) -> FastAPI:     app = FastAPI(         on_startup=[fn.partial(startup_db, db)]         if on_startup is None else on_startup,         on_shutdown=[fn.partial(shutdown_db, db)]         if on_shutdown is None else on_shutdown,     )     graphql_app = GraphQLRouter(         schema,         context_getter=fn.partial(get_context, db),     )     app.include_router(graphql_app, prefix='\/graphql')     return app   def main() -> None:     database = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=settings.DB_NAME,         ),     )     app = get_app(database)     uvicorn.run(         app,         host='0.0.0.0',         port=settings.PORT,     )   if __name__ == '__main__':     main() <\/code><\/pre>\n<h3>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/h3>\n<p>\u041c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u043f\u043e\u0442\u044b\u043a\u0430\u0442\u044c\u0441\u044f \u0440\u0443\u043a\u0430\u043c\u0438, \u043d\u043e \u043c\u044b \u0436\u0435 \u043f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044b.<\/p>\n<p>\u0418\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438 Strawberry \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0445\u0435\u043c\u0443. \u041d\u043e \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0440\u0438\u0433\u043e\u0442\u043e\u0432\u0438\u043c \u0441\u0435\u0431\u0435 \u0442\u0443\u043b\u0438\u043d\u0433.<\/p>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u043e\u043f\u0438\u0448\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432. \u0412\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0430\u0437\u043e\u0432\u044b\u043c \u043a\u043b\u0430\u0441\u0441\u043e\u043c \u0438\u0437 Strawberry, \u043d\u043e \u0434\u043e\u043f\u0438\u043b\u0438\u043c \u0435\u0433\u043e \u043d\u0430\u043f\u0438\u043b\u044c\u043d\u0438\u043a\u043e\u043c.<\/p>\n<ul>\n<li>\n<p>\u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0435\u043c query \u0438\u0437 \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430 \u0438 \u043f\u0435\u0440\u0435\u0434\u0435\u043b\u0430\u0435\u043c \u0442\u0430\u043a, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b \u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 query (\u0443 \u043d\u0430\u0441 \u043d\u0435\u0442 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445 \u0440\u0443\u0447\u0435\u043a).<\/p>\n<\/li>\n<li>\n<p>\u0418\u043c\u043f\u043b\u0435\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u043c request \u2013 \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0441\u0442\u0443\u0447\u0430\u0442\u044c\u0441\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u043c httpx-\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c \u0432 \u0441\u0435\u0440\u0432\u0435\u0440 (\u043e\u0431 \u044d\u0442\u043e\u043c \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435).<\/p>\n<\/li>\n<li>\n<p>\u0423\u043f\u0440\u043e\u0441\u0442\u0438\u043c decode \u0438 _build_body.<\/p>\n<\/li>\n<\/ul>\n<p>tests\/fixtures\/graphql_client.py:<\/p>\n<pre><code class=\"python\">import json import typing as tp  import databases from httpx import AsyncClient from httpx._types import HeaderTypes, RequestFiles from strawberry.test import BaseGraphQLTestClient, Response   class GraphQLTestClient(BaseGraphQLTestClient):      def __init__(self, client: AsyncClient, db: databases.Database):         self._client = client         self.db = db      async def query(         self,         query: str,         variables: tp.Optional[tp.Dict[str, tp.Any]] = None,         headers: tp.Optional[tp.Dict[str, object]] = None,         asserts_errors: tp.Optional[bool] = True,         files: tp.Optional[tp.Dict[str, object]] = None,     ) -> Response:         \"\"\"Modifying query to return only sync.\"\"\"         body = self._build_body(query, variables, files)          resp = await self.request(body, headers, files)         raw_data = self._decode(resp, type='multipart' if files else 'json')          response = Response(             errors=raw_data.get('errors'),             data=raw_data.get('data'),             extensions=raw_data.get('extensions'),         )         if asserts_errors:             assert response.errors is None, response.errors          return response      async def request(         self,         body: dict[str, object],         headers: tp.Optional[dict[str, object]] = None,         files: tp.Optional[dict[str, object]] = None,     ):         \"\"\"Implement actual request.\"\"\"         return await self._client.post(             '\/graphql\/',             json=None if files else body,             data=body if files else None,             files=tp.cast(tp.Optional[RequestFiles], files),             headers=tp.cast(tp.Optional[HeaderTypes], files),             follow_redirects=True,         )      def _build_body(         self,         query: str,         variables: tp.Optional[dict[str, tp.Mapping]] = None,  # type:ignore         files: tp.Optional[dict[str, object]] = None,     ) -> dict[str, object]:         \"\"\"Build body to ignore files.\"\"\"          body: dict[str, object] = {'query': query}          if variables:             body['variables'] = variables          if files:             assert variables is not None             assert files is not None             file_map = self._build_multipart_file_map(variables, files)              body = {                 'operations': json.dumps(body),                 'map': json.dumps(file_map),             }         return body      def _decode(         self,         response,         type: tp.Literal['multipart', 'json'],     ):         \"\"\"Always decode to json.\"\"\"         return response.json()<\/code><\/pre>\n<p>\u041d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043e\u043f\u0438\u0448\u0435\u043c \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b:<\/p>\n<ul>\n<li>\n<p>event_loop;<\/p>\n<\/li>\n<li>\n<p>prepare_db \u2013 \u043f\u0440\u043e\u0433\u043d\u0430\u0442\u044c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0448\u0430\u0431\u043b\u043e\u043d \u0431\u0430\u0437\u044b \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0434\u0430\u043c\u043f-\u0440\u0435\u0441\u0442\u043e\u0440;<\/p>\n<\/li>\n<li>\n<p>recreate_db \u2013 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0431\u0430\u0437\u044b \u0438\u0437 \u0448\u0430\u0431\u043b\u043e\u043d\u0430;<\/p>\n<\/li>\n<li>\n<p>client \u2013 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a \u043d\u0430\u0448\u0435\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0443.<\/p>\n<\/li>\n<\/ul>\n<p>\u0427\u0443\u0442\u044c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e \u043a\u043b\u0438\u0435\u043d\u0442. \u041c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c AsyncClient \u0438\u0437 httpx \u0438 \u043f\u043e\u0434\u0441\u043e\u0432\u044b\u0432\u0430\u0435\u043c \u0432 \u043d\u0435\u0433\u043e \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0434\u0435\u043b\u0430\u0442\u044c \u043e\u0431\u044b\u0447\u043d\u044b\u0435 http-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0432\u043c\u0435\u0441\u0442\u043e \u043c\u043e\u043a\u043e\u0432. force_rollback = True \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0432 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438. \u042d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u0437 \u0448\u0430\u0431\u043b\u043e\u043d\u0430, \u043d\u043e \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0438\u0437-\u0437\u0430 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0445 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439 \u0438 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u0441\u0430\u0439\u0434-\u044d\u0444\u0444\u0435\u043a\u0442\u0430\u043c\u0438 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445. \u0415\u0441\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u0441\u043b\u043e\u043c\u0430\u043b\u043e\u0441\u044c \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0431\u0430\u0437\u044b, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u044b\u0441\u0442\u0430\u0432\u0438\u0442\u044c force_rollback = False.<\/p>\n<p>\u0418\u0442\u043e\u0433\u043e \u0432 tests\/fixtures\/clients.py:<\/p>\n<pre><code class=\"python\">import asyncio  import databases import pytest import pytest_asyncio import settings from httpx import AsyncClient from run import get_app from tests.fixtures.graphql_client import GraphQLTestClient from yoyo import get_backend, read_migrations  TEST_DB_NAME = 'test_{name}'.format(name=settings.DB_NAME)   @pytest.fixture(scope='session') def event_loop():     return asyncio.new_event_loop()   @pytest_asyncio.fixture(autouse=True, scope='session') async def prepare_db():     postgres_db = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name='postgres',         ),     )     async with postgres_db as create_conn:         await create_conn.execute(             'drop database if exists {name};'.format(name=TEST_DB_NAME),         )         await create_conn.execute(             'drop database if exists {name}_template;'.format(                 name=TEST_DB_NAME,             ),         )         await create_conn.execute(             'create database {name};'.format(name=TEST_DB_NAME),         )      backend = get_backend(         settings.MIGRATIONS_CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=TEST_DB_NAME,         ),     )     migrations = read_migrations('.\/migrations')     with backend.lock():         backend.apply_migrations(backend.to_apply(migrations))     del backend  # connection is not released otherwise      async with postgres_db as template_conn:         await template_conn.execute(             'select pg_terminate_backend(pid) '             'from pg_stat_activity'             \" where datname = \\'{name}\\'\".format(                 name=TEST_DB_NAME,             ),         )         await template_conn.execute(             'create database {name}_template template {name};'.format(                 name=TEST_DB_NAME,             ),         )      try:         yield     finally:         async with postgres_db as drop_conn:             await drop_conn.execute(                 'drop database {name};'.format(name=TEST_DB_NAME),             )             await drop_conn.execute(                 'drop database {name}_template;'.format(name=TEST_DB_NAME),             )   async def recreate_db() -> None:     postgres_db = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name='postgres',         ),     )     async with postgres_db as drop_conn:         await drop_conn.execute(             'drop database {name} with (FORCE);'.format(                 name=TEST_DB_NAME,             ),         )         await drop_conn.execute(             'create database {name} template {name}_template;'.format(                 name=TEST_DB_NAME,             ),         )   @pytest_asyncio.fixture() async def client():     # run in transaction - makes tests faster     # but it can cause problems in case of concurrency     force_rollback = True      database = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=TEST_DB_NAME,         ),         force_rollback=force_rollback,     )     async with database as db:         async with AsyncClient(             app=get_app(db=database, on_startup=[], on_shutdown=[]),             base_url='http:\/\/test',         ) as test_client:             graphql_client = GraphQLTestClient(test_client, db)             try:                 yield graphql_client             except Exception:                 pass     if not force_rollback:         await recreate_db()<\/code><\/pre>\n<p>\u0412 \u0444\u0438\u043d\u0430\u043b\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043d\u0430\u0448\u0438 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0432 tests\/conftest.py:<\/p>\n<pre><code class=\"python\">pytest_plugins = (     'tests.fixtures.clients', )<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442 \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0432 tests\/test_users.py. \u041e\u043d \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442 \u043c\u0443\u0442\u0430\u0446\u0438\u044e \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u043e\u0434\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442 resolver \u0442\u0438\u043f\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:<\/p>\n<pre><code class=\"python\">import pytest from tests.fixtures.clients import GraphQLTestClient  pytestmark = [     pytest.mark.asyncio, ]   create_user_query = \"\"\"     mutation createUser(         $input: CreateUserInput!     ) {         createUser(input: $input) {             id         }     } \"\"\"   async def test_create_user(     client: GraphQLTestClient,     mocker, ):     resp = await client.query(         query=create_user_query,         variables={             'input': {                 'name': 'John Doe',             },         },     )     assert resp.data is not None     assert resp.data['createUser'] == {'id': mocker.ANY}<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u043c:<\/p>\n<pre><code class=\"bash\">poetry run pytest<\/code><\/pre>\n<h3>\u0422\u0438\u043f\u044b \u0434\u043b\u044f \u043a\u043d\u0438\u0433<\/h3>\n<p>\u041f\u043e \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0438 \u0441 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438 \u043e\u043f\u0438\u0448\u0435\u043c \u043c\u0443\u0442\u0430\u0446\u0438\u044e \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u043d\u0438\u0433, \u043d\u043e \u0441 \u0442\u0438\u043f\u043e\u043c \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435\u043c \u0431\u0443\u0434\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0435\u0435.<\/p>\n<p>\u0412 \u0442\u0438\u043f\u0435, \u043f\u043e \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044e \u0441 \u0441 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438, \u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f resolver \u043d\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u2013 user. \u0412\u043d\u0443\u0442\u0440\u0438 \u043e\u043d \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 dataloader \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u0438\u0432 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u0438\u0437 root \u2013 pydantic-\u043c\u043e\u0434\u0435\u043b\u0438 Book \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0442\u0438\u043f\u0430 BookType.<\/p>\n<p>\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043a\u043d\u0438\u0433 \u0441 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0435\u0439 \u043e\u0444\u043e\u0440\u043c\u0438\u043c \u043f\u043e <a href=\"https:\/\/relay.dev\/graphql\/connections.htm\" rel=\"noopener noreferrer nofollow\">\u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438 \u043e\u0442 relay<\/a>. \u042d\u0442\u0443 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442 \u0438 <a href=\"https:\/\/graphql.org\/learn\/pagination\/#connection-specification\" rel=\"noopener noreferrer nofollow\"><u>\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0430\u0439\u0442 GraphQL<\/u><\/a>.<\/p>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0442\u0438\u043f PageInfo \u0441 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u043c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0438 generic-\u0442\u0438\u043f\u044b Connection, Edge \u0441 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u0440\u0435\u0431\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e. \u0412 Connection, \u0432 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u043a edges \u0438\u0437 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 relay, \u0434\u043e\u0431\u0430\u0432\u0438\u043c nodes. \u041f\u043e\u0434\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043e \u0432 <a href=\"https:\/\/gitlab.com\/-\/graphql-explorer\" rel=\"noopener noreferrer nofollow\"><u>GraphQL API GitLab&#8217;\u0430<\/u><\/a>. \u042d\u0442\u043e \u0431\u044b\u0432\u0430\u0435\u0442 \u0443\u0434\u043e\u0431\u043d\u043e.<\/p>\n<p>\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0443\u0442 after \u0438 first, \u043f\u0440\u043e \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u0440\u0430\u043d\u0435\u0435 \u043f\u0438\u0441\u0430\u043b\u0438 \u0432 \u043c\u043e\u0434\u0435\u043b\u044f\u0445.<\/p>\n<p>\u0418\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0444\u0430\u0439\u043b \u0432 src\/books\/gql.py:<\/p>\n<pre><code class=\"python\">from typing import Generic, TypeVar, cast  import strawberry from typing import Annotated import strawberry from settings import DEFAULT_LIMIT from src.context import Context from src.books.models import (     CreateBookInput,     Book,     create_book,     get_books, ) from strawberry.types import Info  from src.users.gql import UserType   @strawberry.type class PageInfo:     has_next_page: bool     has_previous_page: bool     start_cursor: str | None     end_cursor: str | None   GenericType = TypeVar('GenericType')   @strawberry.type class Connection(Generic[GenericType]):     page_info: PageInfo     edges: list['Edge[GenericType]']     nodes: list['GenericType']   @strawberry.type class Edge(Generic[GenericType]):     node: GenericType     cursor: str   @strawberry.experimental.pydantic.type(model=Book, name='Book') class BookType():     id: strawberry.auto     title: strawberry.auto      @strawberry.field     async def user(         self,         root: Book,         info: Info[Context, None],     ) -> UserType:         user = await info.context.user_loader.load(             key=root.user_id,         )         if not user:             raise ValueError('user not found')         return user   @strawberry.type(name='Query', extend=True) class BookQuery:      @strawberry.field(name='books')     async def books(         self,         info: Info[Context, None],         after: str | None = None,         first: int | None = DEFAULT_LIMIT,     ) -> Connection[BookType]:         first = (first if first else DEFAULT_LIMIT) + 1          edges = [             Edge(                 cursor=str(book.id),                 node=book,             )             for book in cast(                 list[BookType],                 await get_books(                     db=info.context.db,                     after=after,                     first=first,                 ),             )         ]         has_next_page = len(edges) == first         edges = edges[:-1]          return Connection(             page_info=PageInfo(                 has_next_page=has_next_page,                 has_previous_page=after is not None,                 start_cursor=edges[0].cursor if edges else None,                 end_cursor=edges[-1].cursor if edges else None,             ),             edges=edges,             nodes=[edge.node for edge in edges],         )   @strawberry.experimental.pydantic.input(     model=CreateBookInput, name='CreateBookInput', ) class CreateBookInputType:     title: strawberry.auto     user_id: strawberry.auto   @strawberry.type(name='Mutation', extend=True) class BookMutation:      @strawberry.mutation()     async def create_book(         self,         info: Info[Context, None],         create_input_gql: Annotated[             CreateBookInputType,             strawberry.argument(name='input')         ],     ) -> BookType:         create_input = create_input_gql.to_pydantic()         return cast(             BookType,             await create_book(                 db=info.context.db,                 create_input=create_input,             ),         ) <\/code><\/pre>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043a\u043d\u0438\u0433\u0438 \u0432 \u0441\u0445\u0435\u043c\u0443. \u041c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c run.py. \u0422\u0435\u043f\u0435\u0440\u044c \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"python\">import functools as fn import typing as tp  import databases import settings import strawberry import uvicorn from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig  from src.books.gql import BookMutation, BookQuery from src.context import get_context from src.users.gql import UserMutation, UserQuery    @strawberry.type class Query(     UserQuery,     BookQuery, ):     \"\"\"Query.\"\"\"   @strawberry.type class Mutation(     BookMutation,     UserMutation, ):     \"\"\"Mutations.\"\"\"   schema = strawberry.Schema(     query=Query,     mutation=Mutation,     config=StrawberryConfig(auto_camel_case=True), )   async def startup_db(db: databases.Database):     await db.connect()   async def shutdown_db(db: databases.Database):     await db.disconnect()   HOOK_TYPE = tp.Optional[     tp.Sequence[tp.Callable[[], tp.Any]] ]   def get_app(     db: databases.Database,     on_startup: HOOK_TYPE = None,     on_shutdown: HOOK_TYPE = None, ) -> FastAPI:     app = FastAPI(         on_startup=[fn.partial(startup_db, db)]         if on_startup is None else on_startup,         on_shutdown=[fn.partial(shutdown_db, db)]         if on_shutdown is None else on_shutdown,     )     graphql_app = GraphQLRouter(         schema,         context_getter=fn.partial(get_context, db),     )     app.include_router(graphql_app, prefix='\/graphql')     return app   def main() -> None:     database = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=settings.DB_NAME,         ),     )     app = get_app(database)     uvicorn.run(         app,         host='0.0.0.0',         port=settings.PORT,     )   if __name__ == '__main__':     main() <\/code><\/pre>\n<p>\u041d\u0430\u043f\u0438\u0448\u0435\u043c \u043d\u0430 \u0432\u0441\u0451 \u044d\u0442\u043e \u0434\u0435\u043b\u043e \u0442\u0435\u0441\u0442\u044b \u0432 tests\/test_books.py:<\/p>\n<pre><code class=\"python\">import asyncio import pytest from src.books.models import CreateBookInput, create_book from src.users.models import CreateUserInput, create_user from tests.fixtures.clients import GraphQLTestClient from tests.test_users import create_user_query pytestmark = [     pytest.mark.asyncio, ]   create_book_query = \"\"\"     mutation createBook(         $input: CreateBookInput!     ) {         createBook(input: $input) {             id             user {                 id             }         }     } \"\"\"  books_query = \"\"\"     query books(         $after: String         $first: Int     ) {         books(             after: $after             first: $first         ) {             pageInfo {                 hasNextPage                 hasPreviousPage                 startCursor                 endCursor             }             edges {                 cursor                 node {                     id                 }             }             nodes {                 id             }         }     } \"\"\"   async def test_create_book(     client: GraphQLTestClient,     mocker, ):     create_user_resp = await client.query(         query=create_user_query,         variables={             'input': {                 'name': 'Ayn Rand',             },         },     )     user_id = create_user_resp.data['createUser']['id']  # type: ignore     resp = await client.query(         query=create_book_query,         variables={             'input': {                 'title': 'Atlas shrugged',                 'userId': user_id,             },         },     )     assert resp.data is not None     assert resp.data['createBook'] == {         'id': mocker.ANY,         'user': {             'id': user_id,         }     }  async def test_get_books(     client: GraphQLTestClient, ):     user_inputs = [         CreateUserInput(name='Ayn Rand'),         CreateUserInput(name='Fyodor Dostoevsky'),         CreateUserInput(name='J.K. Rowling'),     ]     user_1, user_2, user_3 = await asyncio.gather(         *(create_user(             db=client.db,             create_input=user_input,         ) for user_input in user_inputs)     )     book_inputs = [         CreateBookInput(title='Atlas Shrugged', user_id=user_1.id),         CreateBookInput(title='Anthem', user_id=user_1.id),         CreateBookInput(title='Idiot', user_id=user_2.id),         CreateBookInput(title='Demons', user_id=user_2.id),         CreateBookInput(title='Crime and Punishment', user_id=user_2.id),         CreateBookInput(             title='Harry Potter and the Philosopher Stone',             user_id=user_3.id         ),         CreateBookInput(             title='Harry Potter and the Chamber of Secrets',             user_id=user_3.id         ),         CreateBookInput(             title='Harry Potter and the Prisoner of Azkaban',             user_id=user_3.id         ),     ]     _, book_2, book_3, book_4, book_5, *_ = await asyncio.gather(         *(create_book(             db=client.db,             create_input=book_input,         ) for book_input in book_inputs)     )     books_resp = await client.query(         query=books_query,         variables={             'after': str(book_2.id),             'first': 3,         },     )     assert books_resp.data     assert books_resp.data['books'] == {         'pageInfo': {             'hasNextPage': True,             'hasPreviousPage': True,             'startCursor': str(book_3.id),             'endCursor': str(book_5.id),         },         'edges': [             {                 'cursor': str(book_3.id),                 'node': {'id': book_3.id},             },             {                 'cursor': str(book_4.id),                 'node': {'id': book_4.id},             },             {                 'cursor': str(book_5.id),                 'node': {'id': book_5.id},             }         ],         'nodes': [             {'id': book_3.id},             {'id': book_4.id},             {'id': book_5.id},         ],     } <\/code><\/pre>\n<p>\u0424\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 <a href=\"https:\/\/gitlab.com\/ruitunion\/strawberry-graphql-example\" rel=\"noopener noreferrer nofollow\"><u>\u043d\u0430\u0448\u0435\u043c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438<\/u><\/a>.<\/p>\n<p>Strawberry \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u0440\u0430\u0437\u0432\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u0447\u0430\u0441\u0442\u043e \u043d\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442. \u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043a\u043e\u043c\u043f\u0435\u043d\u0441\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e\u043c \u0432 <a href=\"https:\/\/discord.com\/invite\/3uQ2PaY\" rel=\"noopener noreferrer nofollow\">Discord<\/a>. Maintainer&#8217;\u044b \u0435\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e \u0432 \u0441\u0435\u0442\u0438 \u0438 \u0433\u043e\u0442\u043e\u0432\u044b \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043c\u043e\u0447\u044c \u043f\u043e \u0432\u0441\u0435\u043c \u0432\u043e\u043f\u0440\u043e\u0441\u0430\u043c.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><\/p>\n<div class=\"tm-article-poll-container\"><!--[--><\/p>\n<div class=\"tm-article-poll tm-article-poll_variant-bordered\">\n<div class=\"tm-notice tm-notice_positive tm-article-poll__notice\"><!----><\/p>\n<div class=\"tm-notice__inner\"><!----><\/p>\n<div class=\"tm-notice__content\" data-test-id=\"notice-content\"><!--[--><span>\u0422\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0443\u0447\u0430\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0432 \u043e\u043f\u0440\u043e\u0441\u0435. <a rel=\"nofollow\" href=\"\/kek\/v1\/auth\/habrahabr\/?back=\/ru\/articles\/688876\/&#038;hl=ru\">\u0412\u043e\u0439\u0434\u0438\u0442\u0435<\/a>, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430.<\/span><!--]--><\/div>\n<\/div>\n<\/div>\n<p><!--[--><\/p>\n<div class=\"tm-article-poll__header\">\u0411\u0443\u0434\u0435\u0448\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Strawberry?<\/div>\n<div class=\"tm-article-poll__answers\"><!--[--><\/p>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent tm-article-poll__answer-percent_winning\">68.42% <\/span><span class=\"tm-article-poll__answer-label\">\u0414\u0430, \u0431\u0443\u0434\u0443, \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e \u0438 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e<\/span><span class=\"tm-article-poll__answer-votes\">13<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress tm-article-poll__answer-progress_winning\" style=\"width: 68.42%\"><\/div>\n<\/div>\n<\/div>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent\">26.32% <\/span><span class=\"tm-article-poll__answer-label\">\u041d\u0435\u0442, \u0434\u0430\u0432\u043d\u043e \u0435\u0441\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043b\u0443\u0447\u0448\u0435 \u0438 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u0435\u0435<\/span><span class=\"tm-article-poll__answer-votes\">5<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress\" style=\"width: 26.32%\"><\/div>\n<\/div>\n<\/div>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent\">5.26% <\/span><span class=\"tm-article-poll__answer-label\">\u0414\u0440\u0443\u0433\u043e\u0435, \u043e\u0441\u0442\u0430\u0432\u0438\u043b \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439<\/span><span class=\"tm-article-poll__answer-votes\">1<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress\" style=\"width: 5.26%\"><\/div>\n<\/div>\n<\/div>\n<p><!--]--><\/div>\n<div class=\"tm-article-poll__stats\"> \u041f\u0440\u043e\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043b\u0438 19 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.   \u0412\u043e\u0437\u0434\u0435\u0440\u0436\u0430\u043b\u0438\u0441\u044c 6 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. <\/div>\n<p><!--]--><\/div>\n<p><!--]--><\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/688876\/\"> https:\/\/habr.com\/ru\/articles\/688876\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"\"><figcaption><\/figcaption><\/figure>\n<p>\u041f\u0438\u0448\u0435\u0442\u0435 \u043d\u0430 Python \u0438 \u0434\u0430\u0432\u043d\u043e \u0445\u043e\u0442\u0435\u043b\u0438 \u0437\u0430\u043f\u0440\u044b\u0433\u043d\u0443\u0442\u044c \u043d\u0430 \u043f\u043e\u0435\u0437\u0434 \u0445\u0430\u0439\u043f\u0430 \u043f\u043e <a href=\"https:\/\/graphql.org\/\" rel=\"noopener noreferrer nofollow\"><u>GraphQL<\/u><\/a>, \u043d\u043e \u043d\u0438\u043a\u0430\u043a \u043d\u0435 \u043c\u043e\u0433\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u043c\u0435\u0436\u0434\u0443 <a href=\"https:\/\/graphene-python.org\/\" rel=\"noopener noreferrer nofollow\"><u>Graphene<\/u><\/a> \u0438 <a href=\"https:\/\/ariadnegraphql.org\/\" rel=\"noopener noreferrer nofollow\"><u>Ariadne<\/u><\/a>? \u041f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u0442\u0440\u0435\u0442\u0438\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u2013 <a href=\"https:\/\/strawberry.rocks\/\" rel=\"noopener noreferrer nofollow\"><u>Strawberry<\/u><\/a>.<\/p>\n<p><a href=\"https:\/\/strawberry.rocks\/\" rel=\"noopener noreferrer nofollow\">Strawberry<\/a> \u2013 code-first \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441 \u0431\u043e\u043b\u044c\u0448\u0438\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u0431\u0430\u0442\u0430\u0440\u0435\u0435\u043a. 2.6 \u0442\u044b\u0441. \u0437\u0432\u0451\u0437\u0434 \u0432 <a href=\"https:\/\/github.com\/strawberry-graphql\/strawberry\" rel=\"noopener noreferrer nofollow\"><u>\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u043d\u0430 GitHub<\/u><\/a>. \u0414\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u043e\u0432 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c dataclasses \u0438 pydantic-\u043c\u043e\u0434\u0435\u043b\u0438. \u0418\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0441\u0442\u044c.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u043c \u0433\u0430\u0439\u0434\u0435 \u043c\u044b \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0435\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 (users) \u0438 \u0438\u0445 \u043a\u043d\u0438\u0433 (books).<\/p>\n<ul>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f (user).<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043d\u0438\u0433\u0443 (book) \u0441\u043e \u0441\u0441\u044b\u043b\u043a\u043e\u0439 \u043d\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043d\u0438\u0433\u0438 \u0441\u043e \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u043c\u0438 \u0432 \u043d\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0425\u0440\u0430\u043d\u0438\u0442\u044c \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0431\u0443\u0434\u0435\u043c \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u0437\u043d\u0430\u0447\u0438\u0442, \u043d\u0430\u0448\u0438\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043c \u0431\u0443\u0434\u0435\u0442 IO-bound. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u0443\u0434\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043e\u0434.<\/p>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f:<\/p>\n<ul>\n<li>\n<p>\u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 Strawberry \u0441 \u0434\u0432\u0438\u0436\u043a\u043e\u043c <a href=\"https:\/\/fastapi.tiangolo.com\/\" rel=\"noopener noreferrer nofollow\"><u>fastAPI<\/u><\/a> \u043d\u0430 Python 3.10;<\/p>\n<\/li>\n<li>\n<p>\u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0430\u0437\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c postgreSQL \u0432\u0435\u0440\u0441\u0438\u0438 14 \u0441 python-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 <a href=\"https:\/\/www.encode.io\/databases\/\" rel=\"noopener noreferrer nofollow\">encode\/databases<\/a> \u0438 \u0434\u0432\u0438\u0436\u043a\u043e\u043c <a href=\"https:\/\/magicstack.github.io\/asyncpg\/current\/\" rel=\"noopener noreferrer nofollow\">asyncpg<\/a>;<\/p>\n<\/li>\n<li>\n<p>\u0434\u043b\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f <a href=\"https:\/\/pydantic-docs.helpmanual.io\/\" rel=\"noopener noreferrer nofollow\">pydantic<\/a>;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0441 \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u043e\u0439 <a href=\"https:\/\/relay.dev\/graphql\/connections.htm\" rel=\"noopener noreferrer nofollow\">relay-style<\/a> \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0435\u0439 \u0438 <a href=\"https:\/\/github.com\/graphql\/dataloader\" rel=\"noopener noreferrer nofollow\">dataloader<\/a>&#8216;\u0430\u043c\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043c\u0443\u0442\u0430\u0446\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>\u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0441 <a href=\"https:\/\/docs.pytest.org\/en\/7.1.x\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a>.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0434\u0440\u0430\u0437\u0443\u043c\u0435\u0432\u0430\u0435\u043c, \u0447\u0442\u043e \u0432\u044b \u0443\u0436\u0435 \u0437\u043d\u0430\u043a\u043e\u043c\u044b \u0441 \u043a\u043e\u043d\u0446\u0435\u043f\u0446\u0438\u044f\u043c\u0438 GraphQL. \u0418\u0437\u0443\u0447\u0438\u0442\u044c \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430 <a href=\"https:\/\/graphql.org\/learn\/\" rel=\"noopener noreferrer nofollow\"><u>\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0440\u0442\u0430\u043b\u0435 GraphQL<\/u><\/a>.<\/p>\n<h3>\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/h3>\n<p>\u0414\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u0430\u044f \u0421\u0423\u0411\u0414 postgreSQL \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0430\u044f \u0432 \u043d\u0435\u0439 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<pre><code class=\"bash\">docker run -d --restart=always -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres --name postgres postgres:14.3-alpine  psql \"postgresql:\/\/postgres:postgres@localhost:5432\" -\u0441 'create database strawberry;'<\/code><\/pre>\n<h3>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438<\/h3>\n<p>\u041f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u043b\u043e\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 <a href=\"https:\/\/python-poetry.org\/\" rel=\"noopener noreferrer nofollow\"><u>Poetry<\/u><\/a>. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0435\u0433\u043e:<\/p>\n<pre><code class=\"bash\">pip install poetry<\/code><\/pre>\n<p>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.<\/p>\n<pre><code>poetry init<\/code><\/pre>\n<p>\u0412\u0441\u0451 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043a\u0440\u043e\u043c\u0435 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 \u043f\u0440\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 (dependencies). \u0422\u0430\u043c \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u043c no.<\/p>\n<p>\u0421\u0442\u0430\u0432\u0438\u043c \u043f\u0430\u043a\u0435\u0442\u044b:<\/p>\n<ul>\n<li>\n<p>Strawberry \u2013 \u0441\u0430\u043c\u0430 GraphQL-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0432\u0435\u0440\u0441\u0438\u044e \u0441 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \u0441 fastAPI;<\/p>\n<\/li>\n<li>\n<p>Databases \u2013 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u0430\u043c\u0438. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0432\u0435\u0440\u0441\u0438\u044e \u0441 \u0434\u0440\u0430\u0439\u0432\u0435\u0440\u043e\u043c asyncpg;<\/p>\n<\/li>\n<li>\n<p>Uvicorn \u2013 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f;<\/p>\n<\/li>\n<li>\n<p>Yoyo-migrations \u2013 \u0443\u0442\u0438\u043b\u0438\u0442\u0430 \u0434\u043b\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p>Psycopg2-binary \u2013 \u0434\u0432\u0438\u0436\u043e\u043a, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p>Pypika \u2013 \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 SQL query builder.<\/p>\n<\/li>\n<\/ul>\n<p>\u0418 \u043f\u0430\u043a\u0435\u0442\u044b \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438:<\/p>\n<ul>\n<li>\n<p>pytest \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>pytest-asyncio \u0434\u043b\u044f \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>pytest-mock \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 mock;<\/p>\n<\/li>\n<li>\n<p>mypy \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0442\u0438\u043f\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p>httpx \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043c\u0438.<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"bash\">poetry add 'strawberry-graphql[fastapi]@0.128.0' 'databases[asyncpg]' 'uvicorn[standard]' yoyo-migrations psycopg2-binary pypika  poetry add -D pytest pytest-asyncio pytest-mock mypy httpx<\/code><\/pre>\n<h3>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u0440\u0430\u0437\u0443 \u0432\u0441\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<pre><code class=\"bash\">mkdir migrations touch migrations\/000001.init.sql touch migrations\/000001.init.rollback.sql touch run.py touch settings.py mkdir -p src\/users mkdir -p src\/books touch src\/__init__.py touch src\/context.py touch src\/users\/__init__.py touch src\/users\/gql.py touch src\/users\/models.py touch src\/books\/__init__.py touch src\/books\/gql.py touch src\/books\/models.py mkdir -p tests\/fixtures touch tests\/__init__.py touch tests\/conftest.py touch tests\/test_users.py touch tests\/test_books.py touch tests\/fixtures\/__init__.py touch tests\/fixtures\/clients.py touch tests\/fixtures\/graphql_client.py touch .env touch mypy.ini<\/code><\/pre>\n<h3>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435<\/h3>\n<p>\u0412 .env \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0430\u0437\u0435:<\/p>\n<pre><code>PORT=8002 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB_NAME=postgres<\/code><\/pre>\n<p>\u0411\u0443\u0434\u0435\u043c \u0447\u0438\u0442\u0430\u0442\u044c \u0438\u0437 .env \u0432 settings.py:<\/p>\n<pre><code class=\"python\">import os  PORT = int(os.environ.get('PORT', 8001)) DB_USER = os.environ['POSTGRES_USER'] DB_PASSWORD = os.environ['POSTGRES_PASSWORD'] DB_SERVER = os.environ['POSTGRES_HOST'] DB_PORT = int(os.environ['POSTGRES_PORT']) DB_NAME = os.environ['POSTGRES_DB_NAME'] CONN_TEMPLATE = (     'postgresql+asyncpg:\/\/{user}:{password}@{host}:{port}\/{name}' ) MIGRATIONS_CONN_TEMPLATE = (     'postgresql:\/\/{user}:{password}@{host}:{port}\/{name}' ) DEFAULT_LIMIT = 100<\/code><\/pre>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043a \u0431\u0430\u0437\u0435 \u0432 run.py:<\/p>\n<pre><code class=\"python\">import functools as fn import typing as tp  import databases import settings import strawberry import uvicorn from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig   @strawberry.type class Query():     \"\"\"Query.\"\"\"      @strawberry.field     def hello(self) -> str:         return 'world'   schema = strawberry.Schema(     query=Query,     config=StrawberryConfig(auto_camel_case=True), )   async def startup_db(db: databases.Database):     await db.connect()   async def shutdown_db(db: databases.Database):     await db.disconnect()   HOOK_TYPE = tp.Optional[     tp.Sequence[tp.Callable[[], tp.Any]] ]   def get_app(     db: databases.Database,     on_startup: HOOK_TYPE = None,     on_shutdown: HOOK_TYPE = None, ) -> FastAPI:     app = FastAPI(         on_startup=[fn.partial(startup_db, db)]         if on_startup is None else on_startup,         on_shutdown=[fn.partial(shutdown_db, db)]         if on_shutdown is None else on_shutdown,     )     graphql_app = GraphQLRouter(         schema,     )     app.include_router(graphql_app, prefix='\/graphql')     return app   def main() -> None:     database = databases.Database(         settings.CONN_TEMPLATE.format(             user=settings.DB_USER,             password=settings.DB_PASSWORD,             port=settings.DB_PORT,             host=settings.DB_SERVER,             name=settings.DB_NAME,         ),     )     app = get_app(database)     uvicorn.run(         app,         host='0.0.0.0',         port=settings.PORT,     )   if __name__ == '__main__':     main() <\/code><\/pre>\n<p>\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u043c:<\/p>\n<pre><code class=\"bash\">poetry run python3 run.py<\/code><\/pre>\n<p>\u0418 \u043e\u0442\u043a\u0440\u043e\u0435\u043c \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435: <a href=\"http:\/\/localhost:8002\/graphql\" rel=\"noopener noreferrer nofollow\">http:\/\/localhost:8002\/graphql<\/a>.<\/p>\n<p>\u041e\u0442\u043a\u0440\u043e\u0435\u0442\u0441\u044f GraphiQL Playground. \u0412 \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0434\u0451\u0440\u043d\u0435\u043c \u043d\u0430\u0448\u0443 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e \u0440\u0443\u0447\u043a\u0443:<\/p>\n<pre><code class=\"json\">{   hello }<\/code><\/pre>\n<p>\u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043c:<\/p>\n<pre><code class=\"json\">{   \"data\": {     \"hello\": \"world\"   } }<\/code><\/pre>\n<h3>\u041c\u043e\u0434\u0435\u043b\u0438<\/h3>\n<p>\u041c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u2013 migrations\/000001.init.sql:<\/p>\n<pre><code class=\"sql\">create table users (     id bigserial primary key,     name text not null ); create table books (     id bigserial primary key,     user_id bigint not null references users(id) on delete cascade,     title text not null );<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u043d\u0430\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u2013 migrations\/000001.init.rollback.sql:<\/p>\n<pre><code class=\"sql\">drop table books; drop table users;<\/code><\/pre>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u043c\u043e\u0434\u0435\u043b\u0438, \u0438\u0445 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435.<\/p>\n<p>\u0414\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f pydantic \u043c\u043e\u0434\u0435\u043b\u044f\u043c\u0438.<\/p>\n<p>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u2013 src\/users\/models.py:<\/p>\n<pre><code class=\"python\">from typing import Optional from databases import Database import pydantic from pypika.dialects import PostgreSQLQuery as Query, Table   class User(pydantic.BaseModel):     id: int     name: str   class CreateUserInput(pydantic.BaseModel):     name: str   async def get_users(     db: Database,     ids: Optional[list[int]] = None, ) -> list[User]:     users_tb = Table('users')     query = Query.from_(users_tb).select(users_tb.star)      if ids is not None:         if not ids:             return []         query = query.where(             users_tb.field('id').isin(ids),         )      return [         User(**el._mapping)         for el in await db.fetch_all(query=str(query))     ]   async def create_user(     db: Database,     create_input: CreateUserInput, ) -> User:     users_tb = Table('users')     query = Query.into(users_tb).columns(         tuple(create_input.dict().keys()),     ).returning(users_tb.star)     for value in create_input.dict().values():         query = query.insert(value)     row = await db.fetch_one(query=str(query))     if not row:         raise ValueError('empty row returned')     return User(**row._mapping)  \u041a\u043d\u0438\u0433\u0438 \u2013 src\/books\/models.py: from typing import Optional from databases import Database import pydantic from pypika.dialects import PostgreSQLQuery as Query, Table   class Book(pydantic.BaseModel):     id: int     title: str     user_id: int   class CreateBookInput(pydantic.BaseModel):     title: str     user_id: int   async def get_books(     db: Database,     ids: Optional[list[int]] = None,     after: str | None = None,     first: int | None = None, ) -> list[Book]:     books_tb = Table('books')     query = Query.from_(books_tb).select(books_tb.star)      if ids is not None:         if not ids:             return []         query = query.where(             books_tb.field('id').isin(ids),         )     if after:         query = query.where(             books_tb.field('id').gt(int(after)),         )     if first:         query = query.limit(first)      return [         Book(**el._mapping)         for el in await db.fetch_all(query=str(query))     ]   async def create_book(     db: Database,     create_input: CreateBookInput, ) -> Book:     books_tb = Table('books')     query = Query.into(books_tb).columns(         tuple(create_input.dict().keys()),     ).returning(books_tb.star).insert(         list(create_input.dict().values()),     )     row = await db.fetch_one(query=str(query))     if not row:         raise ValueError('empty row returned')     return Book(**row._mapping)<\/code><\/pre>\n<p>\u0412 get_books \u043c\u044b \u0437\u0430\u0440\u0430\u043d\u0435\u0435 \u043e\u043f\u0438\u0441\u0430\u043b\u0438, \u043f\u043e\u043c\u0438\u043c\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430 \u043f\u043e \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a\u0430\u043c, \u043a\u0443\u0440\u0441\u043e\u0440\u043d\u0443\u044e \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044e:<\/p>\n<ul>\n<li>\n<p>first \u2013 \u0430\u043d\u0430\u043b\u043e\u0433 limit \u0432 limit-offset&#8217;\u043d\u043e\u0439 \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438;<\/p>\n<\/li>\n<li>\n<p>after \u2013 \u043a\u0443\u0440\u0441\u043e\u0440. \u0417\u0434\u0435\u0441\u044c \u0431\u0443\u0434\u0435\u043c \u0441\u0447\u0438\u0442\u0430\u0442\u044c, \u0447\u0442\u043e \u043a\u0443\u0440\u0441\u043e\u0440 \u2013 \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0422\u0438\u043f\u044b \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/h3>\n<p>\u041e\u043f\u0438\u0448\u0435\u043c \u0442\u0438\u043f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u2013 UserType.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0441\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0435\u0439 pydantic-\u0442\u0438\u043f\u043e\u0432 \u0432 \u0442\u0438\u043f\u044b strawberry. strawberry.auto \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u043c\u044b:<\/p>\n<ul>\n<li>\n<p>\u043a\u043e\u043f\u0438\u0440\u0443\u0435\u043c \u043f\u043e\u043b\u044f \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0438, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 model \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 type;<\/p>\n<\/li>\n<li>\n<p>\u0442\u0438\u043f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438;<\/p>\n<\/li>\n<li>\n<p>GraphQL-\u0438\u043c\u044f \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0432 \u043a\u043e\u043d\u0432\u0435\u043d\u0446\u0438\u0438 camelCase.<\/p>\n<\/li>\n<\/ul>\n<p>\u0421\u0440\u0430\u0437\u0443 \u0436\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u044e\u0437\u0435\u0440\u043e\u0432. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043e\u043f\u0438\u0448\u0435\u043c \u0442\u0438\u043f UserQuery. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 extend=True \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u0442\u043e, \u0447\u0442\u043e \u0432 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0435 \u043d\u0430 \u044f\u0437\u044b\u043a GraphQL \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f extend type Query.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u043c \u0442\u0438\u043f\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u043c resolver \u0441 \u0438\u043c\u0435\u043d\u0435\u043c user. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e user \u0438 \u043f\u043e\u0432\u0435\u0441\u0438\u0442\u044c \u043d\u0430 \u043d\u0435\u0451 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 field. \u041e\u043d \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 id \u0442\u0438\u043f\u0430 int \u2013 \u044d\u0442\u043e \u0430\u0439\u0434\u0438\u0448\u043d\u0438\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>Info[Context, None] \u2013 \u044d\u0442\u043e \u0432\u043d\u0443\u0442\u0440\u044f\u043d\u043a\u0430 Strawberry. \u041f\u0440\u043e \u043d\u0435\u0433\u043e \u0438 \u043f\u0440\u043e user_loader \u2013 \u043f\u043e\u0437\u0436\u0435.<\/p>\n<p>\u0410\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0442\u0438\u043f\u0443 \u044e\u0437\u0435\u0440\u0430 \u043c\u044b \u043e\u043f\u0438\u0448\u0435\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-398359","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/398359","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=398359"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/398359\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=398359"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=398359"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=398359"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}