Compare commits

...

3 Commits

6 changed files with 195 additions and 7 deletions

View File

@ -3,8 +3,27 @@ Fastapi веб приложение реализующее api для общеп
## Описание
Данный проект, это результат выполнения практического домашнего задания интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
### Техническое задание
### Техническое задание
Написать проект на FastAPI с использованием PostgreSQL в качестве БД. В проекте следует реализовать REST API по работе с меню ресторана, все CRUD операции. Для проверки задания, к презентаций будет приложена Postman коллекция с тестами. Задание выполнено, если все тесты проходят успешно.
Даны 3 сущности: Меню, Подменю, Блюдо.
Зависимости:
- У меню есть подменю, которые к ней привязаны.
- У подменю есть блюда.
Условия:
- Блюдо не может быть привязано напрямую к меню, минуя подменю.
- Блюдо не может находиться в 2-х подменю одновременно.
- Подменю не может находиться в 2-х меню одновременно.
- Если удалить меню, должны удалиться все подменю и блюда этого меню.
- Если удалить подменю, должны удалиться все блюда этого подменю.
- Цены блюд выводить с округлением до 2 знаков после запятой.
- Во время выдачи списка меню, для каждого меню добавлять кол-во подменю и блюд в этом меню.
- Во время выдачи списка подменю, для каждого подменю добавлять кол-во блюд в этом подменю.
- Во время запуска тестового сценария БД должна быть пуста.
В папке ./postman_scripts находятся фалы тестов Postman, для тестирования функционала проекта.
## Возможности
В проекте реализованы 3 сущности: Menu, SubMenu и Dish. Для каждого них реализованы 4 метода http запросов: GET, POST, PATCH и DELETE c помощью которых можно управлять данными.
@ -18,10 +37,11 @@ Fastapi веб приложение реализующее api для общеп
Остальное добавится автоматически на этапе установки.
## Установка
### Linux
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
Установите систему управления зависимостями
> `$ pip install poetry`
> `$ pip[x] install poetry`
Клонируйте репозиторий
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
@ -34,6 +54,14 @@ Fastapi веб приложение реализующее api для общеп
Создастся виртуальное окружение и установятся зависимости
Файл example.env является образцом файла .env, который необходимо создать перед запуском проекта.
В нем указанны переменные необходимые для подключения к БД.
Созданим файл .env
>`$ cp ./example.env ./.env`
Далее отредактируйте .env файл в соответствии с Вашими данными подключения к БД
## Запуск
Запуск проекта возможен в 2х режимах:
- Запуск в режиме "prod" с ключем --run-server
@ -49,11 +77,11 @@ Fastapi веб приложение реализующее api для общеп
и запускаем проект в соответстующем режиме
>`$ python manage.py --ключ`
>`$ python[x] manage.py --ключ`
вместо этого, так же допускается и другой вариант запуска одной командой без предварительной активации окружения
>`$ poetry run python manage.py --ключ`
>`$ poetry run python[x] manage.py --ключ`
## TODO
@ -65,6 +93,6 @@ Fastapi веб приложение реализующее api для общеп
- Сергей Ванюшкин <pi3c@yandex.ru>
## Лицензия
Распространяется под [MIT лицензией](https://www.opensource.org/licenses/mit-license.php).
Подробнее на русском в файле LICENSE.md
Распространяется под [MIT лицензией](https://mit-license.org/).

View File

@ -18,6 +18,17 @@ class Settings(BaseSettings):
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
)
@property
def TESTDATABASE_URL_asyncpg(self):
"""
Возвращает строку подключения к БД необходимую для SQLAlchemy
"""
return (
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}"
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}_test"
)
model_config = SettingsConfigDict(env_file=".env")

76
poetry.lock generated
View File

@ -101,6 +101,17 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
[[package]]
name = "certifi"
version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
name = "click"
version = "8.1.7"
@ -276,6 +287,51 @@ files = [
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "1.0.2"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
version = "0.26.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
[[package]]
name = "idna"
version = "3.6"
@ -578,6 +634,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.23.3"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"},
{file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"},
]
[package.dependencies]
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "python-dotenv"
version = "1.0.0"
@ -751,4 +825,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "84fe024aa665ddad3077ca7e73054d1a5cb019a9a3e78af917922433ff4b3d8c"
content-hash = "65d9e5359044da0053c55ad7cd84666c115837d7e6da4f72d41fde0ad349c16b"

View File

@ -14,6 +14,8 @@ asyncpg = "^0.29.0"
pydantic-settings = "^2.1.0"
psycopg2-binary = "^2.9.9"
email-validator = "^2.1.0.post1"
pytest-asyncio = "^0.23.3"
httpx = "^0.26.0"
[tool.poetry.group.dev.dependencies]
@ -25,3 +27,7 @@ build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
pythonpath = ". fastfood"
filterwarnings = [
"ignore::UserWarning",
"ignore::DeprecationWarning"
]

48
tests/conftest.py Normal file
View File

@ -0,0 +1,48 @@
import asyncio
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastfood.app import create_app
from fastfood.config import settings
from fastfood.dbase import get_async_session
from fastfood.models import Base
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
async_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
)
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="function", autouse=True)
async def db_init():
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
@pytest.fixture(scope="session")
def app():
app = create_app()
app.dependency_overrides[get_async_session] = get_test_session
yield app

21
tests/test_api.py Normal file
View File

@ -0,0 +1,21 @@
import pytest
from httpx import AsyncClient
url = "http://localhost:8000/api/v1/menus"
@pytest.mark.asyncio
async def test_read_menus(app):
async with AsyncClient(app=app, base_url=url) as ac:
response = await ac.get("/")
assert response.status_code == 200
assert response.json() == []
@pytest.mark.asyncio
async def test_write_menu(app):
async with AsyncClient(app=app, base_url=url) as ac:
response = await ac.post("/", json={"title": "ddd", "description": "hh"})
assert response.status_code == 201
assert response.json()["title"] == "ddd"