diff --git a/.env b/.env new file mode 100644 index 0000000..24c971e --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=pred-city-env-service +DB_HOST=db +DB_PORT=5432 + +DB_DATABASE_TEST=pred-city-env-service-test +DB_HOST_TEST=db-test +DB_PORT_TEST=5432 diff --git a/README.md b/README.md index 21bd929..16232da 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # pred-city-env -Проект "Прогнозирование негативных аспектов развития городской среды" на БММ 2023 +>Проект "Прогнозирование негативных аспектов развития городской среды" на БММ 2023. +--- + +## Цели +... + +## Архитектура +Архитектуру проекта можно разделить на 4 части: +- База данных для хранения результатов анализа +- [ML модель](ml_research/README.md) для прогнозирования негативных аспектов +- [Frontend](frontend/README.md) для визуализации данных +- [Backend](backend/README.md) для предоставления доступа клиентам diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..7b60253 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# other +.idea/ +poetry.lock diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..33bd3cf --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.10-slim + +WORKDIR /backend_app + +RUN apt-get update && apt-get install -y libpq-dev netcat + +COPY *.py pytest.ini pyproject.toml ./ +COPY core/ ./core/ +COPY routers/ ./routers/ +# COPY tasks/ ./tasks/ +COPY tests/ ./tests/ + +# RUN pip3 install -r requirements.txt +RUN poetry install + +COPY entrypoint.sh . +RUN sed -i 's/\r$//g' entrypoint.sh +RUN chmod +x entrypoint.sh + +ENTRYPOINT ["/backend_app/entrypoint.sh"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e70d0af --- /dev/null +++ b/backend/README.md @@ -0,0 +1,13 @@ +# Backend сервиса + +## Цели +... + +## Архитектура +... + +## JSON:API +... + +## Примеры +... diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..f4561dd --- /dev/null +++ b/backend/config.py @@ -0,0 +1,24 @@ +import os + +from pydantic import PostgresDsn +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + DB_DATABASE: str = os.environ.get("DB_DATABASE", "pred-city-env-service") + DB_USERNAME: str = os.environ.get("DB_USERNAME", "postgres") + DB_PASSWORD: str = os.environ.get("DB_PASSWORD", "postgres") + DB_HOST: str = os.environ.get("DB_HOST", "localhost") + DB_PORT: int = os.environ.get("DB_PORT", 5432) + + DB_DATABASE_TEST: str = os.environ.get( + "DB_DATABASE_TEST", "pred-city-env-service-test" + ) + DB_HOST_TEST: str = os.environ.get("DB_HOST_TEST", "localhost") + DB_PORT_TEST: int = os.environ.get("DB_PORT_TEST", 5432) + + SQLALCHEMY_DATABASE_URL: PostgresDsn = f"postgresql+asyncpg://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_DATABASE}" + SQLALCHEMY_DATABASE_TEST_URL: PostgresDsn = f"postgresql+asyncpg://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST_TEST}:{DB_PORT_TEST}/{DB_DATABASE_TEST}" + + +settings = Settings() diff --git a/backend/core/auth.py b/backend/core/auth.py new file mode 100644 index 0000000..29ff899 --- /dev/null +++ b/backend/core/auth.py @@ -0,0 +1,66 @@ +from datetime import datetime, timedelta +from typing import Optional + +from config import settings +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.ext.asyncio import AsyncSession + +from .database import get_session +from .model_utils import get_user_by_username + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl=settings.TOKEN_URL) +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def create_access_token(subject: str, expires_delta: Optional[timedelta] = None) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(hours=8) + to_encode = {"sub": str(subject), "exp": expire} + encoded_jwt = jwt.encode( + to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM + ) + return encoded_jwt + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def decode_token(token): + try: + payload = jwt.decode( + token, settings.JWT_SECRET_KEY, algorithms=settings.JWT_ALGORITHM + ) + username = payload.get("sub") + if username is None: + raise JWTError + except JWTError: + return None + return username + + +async def get_current_user( + db: AsyncSession = Depends(get_session), token: str = Depends(oauth2_scheme) +): + username = decode_token(token) + if username is None: + raise HTTPException( + status_code=401, + detail="Invalid username or password", + ) + user = await get_user_by_username(username, db) + if user is None: + raise HTTPException( + status_code=401, + detail="Invalid username or password", + ) + return user diff --git a/backend/core/database.py b/backend/core/database.py new file mode 100644 index 0000000..7299ca6 --- /dev/null +++ b/backend/core/database.py @@ -0,0 +1,21 @@ +from typing import AsyncGenerator + +from config import settings +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URL) +Base = declarative_base() +async_session = sessionmaker( + engine, class_=AsyncSession, expire_on_commit=False, autocommit=False +) + + +async def init_db(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session() as session: + yield session diff --git a/backend/core/init_data_db.py b/backend/core/init_data_db.py new file mode 100644 index 0000000..6515460 --- /dev/null +++ b/backend/core/init_data_db.py @@ -0,0 +1,14 @@ +import asyncio +import random + +import pandas as pd + +from .database import get_session, init_db + + +async def _init_all_db(): + pass + + +if __name__ == "__main__": + asyncio.run(_init_all_db()) diff --git a/backend/core/model_utils.py b/backend/core/model_utils.py new file mode 100644 index 0000000..b45f998 --- /dev/null +++ b/backend/core/model_utils.py @@ -0,0 +1,56 @@ +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import func + +from .database import Base, engine +from .models import Car, Location + + +async def get_zip_codes(db: AsyncSession): + return (await db.scalars(select(Location.zip))).all() + + +async def get_user_by_id(id: int, db: AsyncSession) -> User: + query = select(User).where(User.id == id) + return (await db.scalars(query)).first() + + +async def get_user_by_username(username: str, db: AsyncSession) -> User: + query = select(User).where(User.username == username) + return (await db.scalars(query)).first() + + +async def get_count(db: AsyncSession, model: Base): + return (await db.scalars(func.count(model.id))).first() + + +async def get_by_id(db: AsyncSession, model: Base, id: int): + return (await db.scalars(select(model).where(model.id == id))).first() + + +async def get_all(db: AsyncSession, model: Base): + return (await db.scalars(select(model))).all() + + +async def get_location(db: AsyncSession, zip: int): + return (await db.scalars(select(Location).where(Location.zip == zip))).first() + + +async def update_cars_position_random(): + query = update(Car).values( + current_loc=select(Location.zip).order_by(func.random() + Car.id).limit(1) + ) + async with engine.begin() as conn: + await conn.execute(query) + + +async def create_model(db: AsyncSession, model: Base): + db.add(model) + await db.commit() + await db.refresh(model) + return model + + +async def delete_model(db: AsyncSession, model: Base): + await db.delete(model) + await db.commit() diff --git a/backend/core/models.py b/backend/core/models.py new file mode 100644 index 0000000..8534cc3 --- /dev/null +++ b/backend/core/models.py @@ -0,0 +1,39 @@ +import random + +import sqlalchemy as sa +from geopy.distance import distance as geodist + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = sa.Column(sa.Integer, primary_key=True, index=True) + username = sa.Column(sa.String, unique=True, index=True) + hashed_password = sa.Column(sa.String) + full_name = sa.Column(sa.String, nullable=True) + created_at = sa.Column(sa.DateTime, server_default=sa.sql.func.now()) + + def __repr__(self): + return f"User(id={self.id},username={self.username})" + + +class Location(Base): + __tablename__ = "locations" + id = sa.Column(sa.Integer, primary_key=True, index=True) + zip = sa.Column(sa.Integer, unique=True, index=True, nullable=False) + city = sa.Column(sa.String(32), nullable=False) + state_name = sa.Column(sa.Text, nullable=False) + lat = sa.Column(sa.Float, nullable=False) + lng = sa.Column(sa.Float, nullable=False) + + @property + def coords(self) -> (float, float): + return (self.lat, self.lng) + + def distance(self, loc: "Location") -> float: + return round(geodist(self.coords, loc.coords).miles, 4) + + def __str__(self): + return f"zip[{self.lat}, {self.lng}]: {self.zip} # {self.city}" diff --git a/backend/core/schemas.py b/backend/core/schemas.py new file mode 100644 index 0000000..5f5d289 --- /dev/null +++ b/backend/core/schemas.py @@ -0,0 +1,139 @@ +from typing import Annotated + +from pydantic import BaseModel, Field + +# import core.models as models + +Zip = Annotated[ + int, Field(ge=0, le=99999, example="705", description="Zip код локации") +] + +Weight = Annotated[int, Field(ge=1, le=1000, example=123, description="Вес груза")] +Description = Annotated[ + str, Field(example="Text about cargo...", description="Описание груза") +] + +CarNumber = Annotated[ + str, + Field(regex=r"^[1-9]\d{3}[A-Z]$", example="1234A", description="Номер автомобиля"), +] + +Lat = Annotated[float, Field(example=10.345, description="Широта")] +Lng = Annotated[float, Field(example=10.345, description="Долгота")] + + +class Location(BaseModel): + zip: Zip + city: str = Field(example="New York", description="Название города") + state_name: str = Field(example="Texas", description="Название штата") + lat: Lat + lng: Lng + + class Config: + orm_mode = True + + +class Car(BaseModel): + id: int + car_number: CarNumber + current_loc: Zip + load_capacity: Weight + + +class CarFull(BaseModel): + id: int + car_number: CarNumber + loc: Location + load_capacity: Weight + + class Config: + orm_mode = True + + +class CarResponse(Car): + class Config: + orm_mode = True + + +class CarPatch(BaseModel): + current_loc: Zip = None + load_capacity: Weight = None + + +class CarWithDistance(BaseModel): + id: int + car_number: CarNumber + distance: float = Field(example="10.11", description="Расстояние до груза") + + +class Cargo(BaseModel): + _id: int + pick_up: Zip + delivery: Zip + weight: Weight + description: Description + + +class CargoFull(BaseModel): + id: int + pick_up_loc: Location + delivery_loc: Location + weight: Weight + description: Description + + class Config: + orm_mode = True + + +class CargoParams(Cargo): + pass + + +class CargoResponse(Cargo): + id: int + + class Config: + orm_mode = True + + +class CargoGet(CargoResponse): + cars: list[CarWithDistance] + + +class CargoPatch(BaseModel): + weight: Weight = None + description: Description = None + + +class CargoList(BaseModel): + id: int + pick_up: Zip + delivery: Zip + count_cars_nerby: int = Field(example="10", description="Количество машин рядом") + + +class CargoDelete(BaseModel): + detail: str = Field(example="status") + + +class CarLocation(BaseModel): + car_number: CarNumber + loc: Location + + class Config: + orm_mode = True + + +class CargoLocation(BaseModel): + id: int + pick_up_loc: Location + delivery_loc: Location + description: Description + + class Config: + orm_mode = True + + +class GeoResponse(BaseModel): + cars: list[CarLocation] + cargo: list[CargoLocation] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100755 index 0000000..cf25c96 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Waiting for postgres..." + +while ! nc -z $DB_HOST $DB_PORT; do + sleep 0.1 +done + +echo "PostgreSQL started" + +exec "$@" diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..50056a8 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,36 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from core.database import init_db +from core.init_data_db import init_data +from routers import geo, gql, users + +app = FastAPI( + title="Аналитика гетто", + description="Предоставление driven-data аналитики по гетто", + version="1.1.0", + license_info={"name": "MIT License", "url": "https://mit-license.org/"}, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(users.router) +app.include_router(geo.router) +app.include_router(gql.router, prefix="/graphql") + + +@app.on_event("startup") +async def on_startup(): + await init_db() + await init_data() + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0") diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..cc830e2 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "backend" +version = "0.1.0" +description = "Сервис для предоставления driven-data аналитики." +authors = ["Your Name "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +fastapi = "^0.100.0" +asyncpg = "^0.28.0" +psycopg2-binary = "^2.9.6" +SQLAlchemy = "^2.0.18" +uvicorn = "^0.22.0" +python-jose = "^3.3.0" +pydantic = "^2.0.2" +pydantic-settings = "^2.0.1" +geopy = "^2.3.0" + + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..a41ad4c --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,4 @@ +# pytest.ini +[pytest] +asyncio_mode=auto +filterwarnings = ignore::DeprecationWarning diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..e9a7a62 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,41 @@ +amqp==5.1.1 +anyio==3.7.0 +asyncpg==0.27.0 +billiard==4.1.0 +celery==5.3.1 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.3.0 +fastapi==0.99.0 +geographiclib==2.0 +geopy==2.3.0 +graphql-core==3.2.3 +greenlet==2.0.2 +h11==0.14.0 +idna==3.4 +iniconfig==2.0.0 +kombu==5.3.1 +numpy==1.25.0 +packaging==23.1 +pandas==2.0.3 +pluggy==1.2.0 +prompt-toolkit==3.0.38 +psycopg2-binary==2.9.6 +pydantic==1.10.10 +pytest==7.4.0 +pytest-asyncio==0.21.0 +python-dateutil==2.8.2 +python-multipart==0.0.6 +pytz==2023.3 +redis==4.6.0 +six==1.16.0 +sniffio==1.3.0 +SQLAlchemy==1.4.48 +starlette==0.27.0 +strawberry-graphql==0.192.0 +typing_extensions==4.7.0 +tzdata==2023.3 +uvicorn==0.22.0 +vine==5.0.0 +wcwidth==0.2.6 diff --git a/backend/routers/geo.py b/backend/routers/geo.py new file mode 100644 index 0000000..c80c162 --- /dev/null +++ b/backend/routers/geo.py @@ -0,0 +1,31 @@ +import core.model_utils as model_utils +import core.models as models +import core.schemas as schemas +from core.database import get_session +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +router = APIRouter(prefix="/geo", tags=["geo"]) + + +@router.get("/location/{zip}", description="Получение локации по zip коду.") +async def get_location(zip: int, db: AsyncSession = Depends(get_session)): + if loc := await model_utils.get_location(db, zip): + return schemas.Location.from_orm(loc) + else: + raise HTTPException( + status_code=404, detail="There is no location for such zip." + ) + + +@router.get("/", description="Получение всех данных о местоположении объектов.") +async def get_geo(db: AsyncSession = Depends(get_session)): + cargo = [ + schemas.CargoLocation.from_orm(cg) + for cg in await model_utils.get_all(db, models.Cargo) + ] + cars = [ + schemas.CarLocation.from_orm(car) + for car in await model_utils.get_all(db, models.Car) + ] + return schemas.GeoResponse(cargo=cargo, cars=cars) diff --git a/backend/routers/gql.py b/backend/routers/gql.py new file mode 100644 index 0000000..89c98a9 --- /dev/null +++ b/backend/routers/gql.py @@ -0,0 +1,71 @@ +import core.models as models +import core.schemas as schemas +import strawberry +from core.database import get_session +from core.model_utils import get_all, get_by_id, get_location +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession +from strawberry.fastapi import GraphQLRouter +from strawberry.types import Info + + +async def get_context(db: AsyncSession = Depends(get_session)): + return {"db": db} + + +@strawberry.experimental.pydantic.type(model=schemas.Location) +class Location: + zip: strawberry.auto + city: strawberry.auto + state_name: strawberry.auto + lat: float + lng: float + + +@strawberry.experimental.pydantic.type(model=schemas.CarFull, all_fields=True) +class Car: + pass + + +@strawberry.experimental.pydantic.type(model=schemas.CargoFull) +class Cargo: + id: strawberry.auto + pick_up_loc: strawberry.auto + delivery_loc: strawberry.auto + weight: strawberry.auto + description: str + + +@strawberry.type +class Query: + @strawberry.field + async def cars(self, info: Info) -> list[Car]: + return [ + Car.from_pydantic(schemas.CarFull.from_orm(c)) + for c in await get_all(info.context["db"], models.Car) + ] + + @strawberry.field + async def get_car(self, info: Info, id: int) -> Car | None: + return Car.from_pydantic( + schemas.CarFull.from_orm( + await get_by_id(info.context["db"], models.Car, id=id) + ) + ) + + @strawberry.field + async def cargo(self, info: Info) -> list[Cargo]: + return [ + Cargo.from_pydantic(schemas.CargoFull.from_orm(c)) + for c in await get_all(info.context["db"], models.Cargo) + ] + + @strawberry.field + async def location(self, info: Info, zip: schemas.Zip) -> Location | None: + return Location.from_pydantic( + schemas.Location.from_orm(await get_location(info.context["db"], zip)) + ) + + +schema = strawberry.Schema(query=Query, types=[Cargo, Car, Location]) +router = GraphQLRouter(schema, context_getter=get_context) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..7b38ea8 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,99 @@ +import asyncio +from typing import AsyncGenerator + +import core.models as models +import pytest +from config import settings +from core.database import Base, get_session +from fastapi.testclient import TestClient +from httpx import AsyncClient +from main import app +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker + +engine_test = create_async_engine(settings.SQLALCHEMY_DATABASE_TEST_URL) +async_session_test = sessionmaker( + engine_test, class_=AsyncSession, expire_on_commit=False, autocommit=False +) +Base.metadata.bind = engine_test + + +async def override_get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_test() as session: + yield session + + +app.dependency_overrides[get_session] = override_get_session + + +@pytest.fixture(autouse=True, scope="session") +async def prepare_database(): + async with engine_test.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield + async with engine_test.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + +@pytest.fixture(scope="session") +def event_loop(request): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +client = TestClient(app) + + +@pytest.fixture(scope="session") +async def ac() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient(app=app, base_url="http://test") as ac: + yield ac + + +@pytest.fixture(scope="session") +async def create_data(): + async with async_session_test() as db: + loc_data = [ + [58045, "Hillsboro", "North Dakota", 47.38241, -97.02686], + [15951, "Saint Michael", "Pennsylvania", 40.3307, -78.77209], + [84076, "Tridell", "Utah", 40.44718, -109.84017], + [5446, "Colchester", "Vermont", 44.5541, -73.21647], + [12933, "Ellenburg", "New York", 44.89118, -73.84512], + [50472, "Saint Ansgar", "Iowa", 43.42265, -92.94644], + [34470, "Ocala", "Florida", 29.20098, -82.08262], + [39092, "Lake", "Mississippi", 32.31489, -89.36833], + ] + + loc_models = [ + models.Location(zip=d[0], city=d[1], state_name=d[2], lat=d[3], lng=d[4]) + for d in loc_data + ] + db.add_all(loc_models) + + car_data = [ + ["9218C", 58045, 215], + ["4075H", 15951, 425], + ["1225K", 5446, 892], + ["3863F", 12933, 416], + ] + + car_models = [ + models.Car(car_number=d[0], current_loc=d[1], load_capacity=d[2]) + for d in car_data + ] + db.add_all(car_models) + + cargo_data = [ + [12933, 58045, 123, "Text1"], + [58045, 12933, 250, "Text2"], + [15951, 58045, 123, "Text3"], + ] + + cargo_models = [ + models.Cargo(pick_up=d[0], delivery=d[1], weight=d[2], description=d[3]) + for d in cargo_data + ] + db.add_all(cargo_models) + await db.commit() + print("DATA LOADED") diff --git a/backend/tests/test_cargo_api.py b/backend/tests/test_cargo_api.py new file mode 100644 index 0000000..c6fc41f --- /dev/null +++ b/backend/tests/test_cargo_api.py @@ -0,0 +1,44 @@ +import pytest + + +async def test_list_cargo(ac, create_data): + response = await ac.get("cargo/", params={"distance": 450}) + assert response.status_code == 200 + assert response.json()[0] == { + "count_cars_nerby": 3, + "delivery": 58045, + "id": 1, + "pick_up": 12933, + } + + +async def test_post_cargo(ac, create_data): + response = await ac.post( + "cargo/", + json={ + "pick_up": 39092, + "delivery": 15951, + "weight": 500, + "description": "Text4", + }, + ) + assert response.status_code == 200 + assert response.json()["id"] + + +async def test_get_cargo(ac, create_data): + response = await ac.get("cargo/1") + assert response.status_code == 200 + assert response.json()["id"] == 1 + + +@pytest.mark.parametrize("data,result", [({"weight": 100}, {"code": 200})]) +async def test_update_cargo(data, result, ac, create_data): + response = await ac.patch("cargo/", params={"cargo_id": 1}, json=data) + assert response.status_code == result["code"] + assert response.json()["weight"] == data["weight"] + + +async def test_delete_cargo(ac, create_data): + response = await ac.delete("cargo/1") + assert response.status_code == 200 diff --git a/backend/tests/test_cars_api.py b/backend/tests/test_cars_api.py new file mode 100644 index 0000000..17ee0b8 --- /dev/null +++ b/backend/tests/test_cars_api.py @@ -0,0 +1,20 @@ +import pytest + + +async def test_list_car(ac, create_data): + response = await ac.get("cars/") + assert response.status_code == 200 + assert response.json()[0]["id"] == 1 + + +async def test_post_car(ac, create_data): + response = await ac.post("cars/", params={"start_location": 34470}) + assert response.status_code == 200 + assert len(response.json()["car_number"]) == 5 + + +@pytest.mark.parametrize("data,result", [({"current_loc": 15951}, {"code": 200})]) +async def test_update_car(data, result, ac, create_data): + response = await ac.patch("cars/", params={"car_id": 1}, json=data) + assert response.status_code == result["code"] + assert response.json()["current_loc"] == data["current_loc"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9171562 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.9" +services: + db: + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data/ + expose: + - ${DB_HOST} + environment: + - POSTGRES_DB=${DB_DATABASE} + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + env_file: + - ./.env + backend: + build: ./backend + command: uvicorn main:app --host 0.0.0.0 --port 8000 + volumes: + - ./backend:/backend_app + ports: + - 8001:8000 + environment: + - DATABASE_URL=postgresql+asyncpg://${DB_USERNAME}:${DB_PASSWORD}@db:${DB_HOST}/${DB_DATABASE} + env_file: + - ./.env + depends_on: + - db + - redis + # frontend: + # build: ./frontend + # ports: + # - 3001:3000 + # env_file: + # - ./.env + # depends_on: + # - backend + +volumes: + postgres_data: diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..9a2be6c --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,7 @@ +# Frontend сервиса + +## Запуск +... + +## Компоненты +... diff --git a/frontend/visual.py b/frontend/visual.py new file mode 100644 index 0000000..2129c4c --- /dev/null +++ b/frontend/visual.py @@ -0,0 +1,17 @@ +# importing required libraries + +import numpy as np +import pandas as pd +import streamlit as st + +# creating a sample data consisting different points + +df = pd.DataFrame( + np.random.randn(800, 2) / [50, 50] + [46.34, -108.7], + columns=["latitude", "longitude"], +) + + +# plotting a map with the above defined points + +st.map(df) diff --git a/ml_research/README.md b/ml_research/README.md new file mode 100644 index 0000000..3024b25 --- /dev/null +++ b/ml_research/README.md @@ -0,0 +1,4 @@ +# Анализ негативных аспектов развития городской среды + +... +Пункты анализа...