diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..987801b --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +ignore = E203,E402,C901,W503,W504,E116,E501,E702,E731,W613,W102,R903,R902,R914,R915,R205,W703,W702,W603 diff --git a/backend/core/init_data_db.py b/backend/core/init_data_db.py index a4eda4a..493de25 100644 --- a/backend/core/init_data_db.py +++ b/backend/core/init_data_db.py @@ -3,15 +3,12 @@ import pathlib import geoalchemy2 as gsa -# import geopandas import shapely -import sqlalchemy as sa from core.auth import get_password_hash from core.model_utils import get_count from .database import async_session, init_db -from .model_utils import get_city_and_districts from .models import City, City_property, District, District_property, User DATA_DIR = pathlib.Path(__file__).parent.parent.joinpath("data") @@ -34,8 +31,7 @@ async def __spb_example_init_data(): geom=dist["geometry"], ) ) - - spb = City( + city = City( title="Санкт-Петербург", properties=City_property(population=5600044, area=1439), districts=districts, @@ -43,7 +39,7 @@ async def __spb_example_init_data(): geom=gsa.shape.from_shape(shapely.geometry.MultiPolygon(), srid=4326), ) async with async_session() as db: - db.add(spb) + db.add(city) await db.commit() print("Successful init data!") diff --git a/backend/core/model_utils.py b/backend/core/model_utils.py index 8efa287..7c5091c 100644 --- a/backend/core/model_utils.py +++ b/backend/core/model_utils.py @@ -1,8 +1,11 @@ +from types import FunctionType + import sqlalchemy as sa from sqlalchemy.ext.asyncio import AsyncSession from .database import Base -from .models import City, City_property, District, District_property, User +from .models import (Block, Block_property, City, City_property, District, + District_property, User) async def get_user_by_id(id: int, db: AsyncSession) -> User: @@ -19,8 +22,12 @@ async def get_count(db: AsyncSession, model: Base): return (await db.scalars(sa.sql.func.count(model.id))).first() +async def get_where(db: AsyncSession, model: Base, condition: FunctionType): + return (await db.scalars(sa.select(model).where(condition(model)))).first() + + async def get_by_id(db: AsyncSession, model: Base, id: int): - return (await db.scalars(sa.select(model).where(model.id == id))).first() + return await get_where(db, model, lambda x: x.id == id) async def get_all(db: AsyncSession, model: Base, limit: int = None): @@ -29,7 +36,19 @@ async def get_all(db: AsyncSession, model: Base, limit: int = None): return (await db.scalars(sa.select(model))).all() +async def get_all_where( + db: AsyncSession, model: Base, condition: FunctionType = None, limit: int = None +): + if limit: + return ( + await db.scalars(sa.select(model).where(condition(model)).limit(limit)) + ).all() + return (await db.scalars(sa.select(model).where(condition(model)))).all() + + def exec_query_geojson(func_query): + """Getting data from the database in the form of GeoJSON""" + async def wrapper(db: AsyncSession, *args, **kwargs): query = func_query(*args, **kwargs) data = (await db.execute(query)).first()[0] @@ -42,17 +61,12 @@ async def wrapper(db: AsyncSession, *args, **kwargs): @exec_query_geojson -def get_blocks_geojson(): - return - - -@exec_query_geojson -def get_districts_geojson(): +def get_blocks_geojson(city_id: int): return ( sa.select( sa.func.json_build_object( "type", - "FeatuerCollection", + "FeatureCollection", "features", sa.func.json_agg( sa.func.json_build_object( @@ -61,30 +75,31 @@ def get_districts_geojson(): "properties", sa.func.json_build_object( "title", - District.title, + Block.title, "population", - District_property.population, + Block_property.population, "area", - District_property.area, + Block_property.area, ), "geometry", - sa.func.ST_AsGeoJSON(District.geom).cast(sa.JSON), + sa.func.ST_AsGeoJSON(Block.geom).cast(sa.JSON), ) ), ) ) - .select_from(District) - .outerjoin(District_property, District.id == District_property.district_id) + .select_from(Block) + .outerjoin(Block_property, Block.id == Block_property.block_id) + .where(Block.city_id == city_id) ) @exec_query_geojson -def get_cities_geojson(): +def get_districts_geojson(city_id: int): return ( sa.select( sa.func.json_build_object( "type", - "FeatuerCollection", + "FeatureCollection", "features", sa.func.json_agg( sa.func.json_build_object( @@ -93,41 +108,28 @@ def get_cities_geojson(): "properties", sa.func.json_build_object( "title", - City.title, + District.title, "population", - City_property.population, + District_property.population, "area", - City_property.area, + District_property.area, ), "geometry", - sa.func.ST_AsGeoJSON(City.geom).cast(sa.JSON), + sa.func.ST_AsGeoJSON(District.geom).cast(sa.JSON), ) ), ) ) - .select_from(City) - .outerjoin(City_property, City.id == City_property.city_id) + .select_from(District) + .outerjoin(District_property, District.id == District_property.district_id) + .where(District.city_id == city_id) ) -async def get_city_and_districts(db: AsyncSession, city_title: str): - query = ( +@exec_query_geojson +def get_cities_geojson(): + return ( sa.select( - sa.func.json_build_object( - "type", - "Feature", - "properties", - sa.func.json_build_object( - "title", - City.title, - "population", - City_property.population, - "area", - City_property.area, - ), - "geometry", - sa.func.ST_AsGeoJSON(City.geom).cast(sa.JSON), - ), sa.func.json_build_object( "type", "FeatureCollection", @@ -139,35 +141,22 @@ async def get_city_and_districts(db: AsyncSession, city_title: str): "properties", sa.func.json_build_object( "title", - District.title, + City.title, "population", - District_property.population, + City_property.population, "area", - District_property.area, + City_property.area, ), "geometry", - sa.func.ST_AsGeoJSON(District.geom).cast(sa.JSON), + sa.func.ST_AsGeoJSON(City.geom).cast(sa.JSON), ) ), - ), + ) ) .select_from(City) - .outerjoin(District, City.id == District.city_id) - .outerjoin(District_property, District.id == District_property.district_id) .outerjoin(City_property, City.id == City_property.city_id) - .where(City.title == city_title) - .group_by( - City.title, - City.geom, - City_property.population, - City_property.area, - ) ) - if data := (await db.execute(query)).first(): - return {"city": data[0], "districts": data[1]} - return {} - async def create_model(db: AsyncSession, model: Base): db.add(model) diff --git a/backend/core/models.py b/backend/core/models.py index b11d1e3..4ecc7c9 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -11,20 +11,24 @@ def geom_init_decorator(cls): def new_init(self, *args, geom, **kwargs): if isinstance(geom, dict): - init( - self, *args, geom=gsa.shape.from_shape(shape(geom), srid=4326), **kwargs - ) - else: - init(self, *args, geom=geom, **kwargs) + geom = gsa.shape.from_shape(shape(geom), srid=4326) + init(self, *args, geom=geom, **kwargs) + @property + def geometry(self): + shape = gsa.shape.to_shape(self.geom) + return shape.__geo_interface__ + + cls.geometry = geometry cls.__init__ = new_init return cls class User(Base): - ROLE_TYPES = [(1, "admin"), (2, "user")] __tablename__ = "users" + ROLE_TYPES = [(1, "admin"), (2, "user")] + id = sa.Column(sa.Integer, primary_key=True, index=True) username = sa.Column(sa.String(32), unique=True, index=True) hashed_password = sa.Column(sa.String) @@ -49,6 +53,10 @@ class District_property(Base): population = sa.Column(sa.Integer, nullable=True) area = sa.Column(sa.Float, nullable=False) + @property + def title(self) -> str: + return self.district.title + district = sa.orm.relationship("District", back_populates="properties") @@ -64,6 +72,10 @@ class Block_property(Base): population = sa.Column(sa.Integer, nullable=True) area = sa.Column(sa.Float, nullable=False) + @property + def title(self) -> str: + return self.block.title + block = sa.orm.relationship("Block", back_populates="properties") @@ -76,6 +88,10 @@ class City_property(Base): population = sa.Column(sa.Integer, nullable=True) area = sa.Column(sa.Float, nullable=False) + @property + def title(self) -> str: + return self.district.title + city = sa.orm.relationship("City", back_populates="properties") @@ -90,11 +106,6 @@ class District(Base): gsa.Geometry(geometry_type="MULTIPOLYGON", srid=4326), nullable=False ) - @property - def geometry(self): - shape = gsa.shape.to_shape(self.geom) - return shape.__geo_interface__ - city = sa.orm.relationship("City", back_populates="districts", lazy="selectin") properties = sa.orm.relationship( "District_property", back_populates="district", uselist=False, lazy="selectin" @@ -111,11 +122,6 @@ class Block(Base): gsa.Geometry(geometry_type="MULTIPOLYGON", srid=4326), nullable=False ) - @property - def geometry(self): - shape = gsa.shape.to_shape(self.geom) - return shape.__geo_interface__ - city = sa.orm.relationship("City", back_populates="blocks", lazy="selectin") properties = sa.orm.relationship( "Block_property", back_populates="block", uselist=False, lazy="selectin" @@ -132,11 +138,6 @@ class City(Base): gsa.Geometry(geometry_type="MULTIPOLYGON", srid=4326), nullable=False ) - @property - def geometry(self): - shape = gsa.shape.to_shape(self.geom) - return shape.__geo_interface__ - districts = sa.orm.relationship("District", back_populates="city", lazy="selectin") blocks = sa.orm.relationship("Block", back_populates="city", lazy="selectin") properties = sa.orm.relationship( diff --git a/backend/core/schemas.py b/backend/core/schemas.py index ef95385..de7d29f 100644 --- a/backend/core/schemas.py +++ b/backend/core/schemas.py @@ -2,7 +2,7 @@ from typing import Annotated import shapely -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, HttpUrl Lat = Annotated[float, Field(example=10.345, description="Широта")] Lng = Annotated[float, Field(example=10.345, description="Долгота")] @@ -55,6 +55,7 @@ class LoginResponse(BaseModel): class Properties(BaseModel): + # title: str population: int = Field(ge=0, description="Население") area: float = Field(gt=0, description="Площадь(км^2)") @@ -127,3 +128,7 @@ class City(BaseModel): class Config: from_attributes = True + + +class LinksList(BaseModel): + links: list[HttpUrl] diff --git a/backend/main.py b/backend/main.py index 029cf40..e24dc58 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,7 +5,7 @@ from core.database import init_db from core.init_data_db import init_data -from routers import geo, users +from routers import geo, others, users app = FastAPI( title="Аналитика гетто", @@ -25,6 +25,7 @@ app.include_router(users.router) app.include_router(geo.router) # app.include_router(gql.router, prefix="/graphql") +app.include_router(others.router) @app.on_event("startup") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 15512f5..054a7d2 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -31,6 +31,7 @@ geopandas = "^0.13.2" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" pytest-asyncio = "^0.21.0" +flake8 = "^6.0.0" [build-system] requires = ["poetry-core"] diff --git a/backend/routers/geo.py b/backend/routers/geo.py index 84cde70..87f3ae0 100644 --- a/backend/routers/geo.py +++ b/backend/routers/geo.py @@ -14,11 +14,23 @@ "/cities", description="Получение списка всех городов.", ) -async def get_cities( - limit: Annotated[int, Query(gt=0, description="Ограничение запросов")] = None, +async def list_cities( + title: Annotated[str, Query(description="Название города")] = None, + limit: Annotated[ + int, Query(ge=1, le=2000, description="Ограничение запросов") + ] = 1500, geojson: Annotated[bool, Query(description="Флаг форматирования в GeoJSON")] = None, db: AsyncSession = Depends(get_session), ): + if title: + if city := await model_utils.get_where( + db, models.City, lambda x: x.title == title + ): + return schemas.CityList.parse_obj(city) + else: + raise HTTPException( + status_code=404, detail="There is no city for such title." + ) if geojson: return await model_utils.get_cities_geojson(db) return [ @@ -34,25 +46,23 @@ async def get_cities( ) async def get_city( id: int, - with_districts: Annotated[ + districts: Annotated[ bool, Query(description="Флаг добавления информации о районах") ] = None, - with_blocks: Annotated[ + blocks: Annotated[ bool, Query(description="Флаг добавления информации о кварталах") ] = None, - with_geometry: Annotated[ - bool, Query(description="Флаг добавления геоданных") - ] = None, + geometry: Annotated[bool, Query(description="Флаг добавления геоданных")] = None, db: AsyncSession = Depends(get_session), ): if data := await model_utils.get_by_id(db, models.City, id): res = schemas.City.parse_obj(data) - if not with_geometry: + if not geometry: del res.geometry - if not with_districts: + if not districts: del res.districts - if not with_blocks: + if not blocks: del res.blocks return res else: @@ -63,21 +73,27 @@ async def get_city( "/districts", description="Получение списка всех районов.", ) -async def get_districts( +async def list_districts( city_id: Annotated[int, Query(gt=0, description="ID города")] = None, - limit: Annotated[int, Query(gt=0, description="Ограничение запросов")] = None, + limit: Annotated[ + int, Query(ge=1, le=5000, description="Ограничение запросов") + ] = 1000, geojson: Annotated[bool, Query(description="Флаг форматирования в GeoJSON")] = None, db: AsyncSession = Depends(get_session), ): if geojson: - return await model_utils.get_districts_geojson(db) + if await model_utils.get_by_id(db, models.City, city_id): + return await model_utils.get_districts_geojson(db, city_id=city_id) + else: + raise HTTPException(status_code=404, detail="There is no city for such id.") cond = lambda x: True if city_id: cond = lambda x: x.city_id == city_id return [ schemas.DistrictList.parse_obj(d) - for d in await model_utils.get_all(db, models.District, limit=limit) - if cond(d) + for d in await model_utils.get_all_where( + db, models.District, condition=cond, limit=limit + ) ] @@ -88,15 +104,13 @@ async def get_districts( ) async def get_district( id: int, - with_geometry: Annotated[ - bool, Query(description="Флаг добавления геоданных") - ] = None, + geometry: Annotated[bool, Query(description="Флаг добавления геоданных")] = None, db: AsyncSession = Depends(get_session), ): if data := await model_utils.get_by_id(db, models.District, id): res = schemas.District.parse_obj(data) - if not with_geometry: + if not geometry: del res.geometry return res else: @@ -107,16 +121,28 @@ async def get_district( "/blocks", description="Получение списка всех кварталов.", ) -async def get_districts( - limit: Annotated[int, Query(gt=0, description="Ограничение запросов")] = None, +async def list_blocks( + city_id: Annotated[int, Query(gt=0, description="ID города")] = None, + limit: Annotated[ + int, Query(ge=1, le=5000, description="Ограничение запросов") + ] = 1000, geojson: Annotated[bool, Query(description="Флаг форматирования в GeoJSON")] = None, db: AsyncSession = Depends(get_session), ): if geojson: - return await model_utils.get_blocks_geojson(db) + if await model_utils.get_by_id(db, models.City, city_id): + return await model_utils.get_blocks_geojson(db, city_id=city_id) + else: + raise HTTPException(status_code=404, detail="There is no city for such id.") + + cond = lambda x: True + if city_id: + cond = lambda x: x.city_id == city_id return [ schemas.BlockList.parse_obj(d) - for d in await model_utils.get_all(db, models.Block, limit=limit) + for d in await model_utils.get_all_where( + db, models.Block, condition=cond, limit=limit + ) ] @@ -125,17 +151,15 @@ async def get_districts( response_model=schemas.Block, description="Получение полной информации по кварталу.", ) -async def get_district( +async def get_block( id: int, - with_geometry: Annotated[ - bool, Query(description="Флаг добавления геоданных") - ] = None, + geometry: Annotated[bool, Query(description="Флаг добавления геоданных")] = None, db: AsyncSession = Depends(get_session), ): if data := await model_utils.get_by_id(db, models.Block, id): res = schemas.Block.parse_obj(data) - if not with_geometry: + if not geometry: del res.geometry return res else: diff --git a/backend/routers/others.py b/backend/routers/others.py new file mode 100644 index 0000000..0d60dbc --- /dev/null +++ b/backend/routers/others.py @@ -0,0 +1,16 @@ +from typing import Annotated + +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, Query +from sqlalchemy.ext.asyncio import AsyncSession + +router = APIRouter(prefix="/others", tags=["others"]) + + +@router.get("/links", description="Парсинг данных на основании внешних ссылок.") +async def get_links(links: schemas.LinksList, db: AsyncSession = Depends(get_session)): + print(links) + return {} diff --git a/frontend/main.py b/frontend/main.py index c7620f7..50265c7 100644 --- a/frontend/main.py +++ b/frontend/main.py @@ -1,5 +1,5 @@ -import json -import pathlib +# import json +# import pathlib import random import folium @@ -25,9 +25,12 @@ def read_df_from_geojson(gj: dict) -> pd.DataFrame: # with open(geojson_path, "r") as f: # geojson = json.load(f) +spb_id = requests.get( + "http://0.0.0.0:8000/geo/cities", params={"title": "Санкт-Петербург"} +).json()["id"] geojson = requests.get( - "http://0.0.0.0:8000/geo/cities", params={"city_title": "Санкт-Петербург"} -).json()["districts"] + "http://0.0.0.0:8000/geo/districts", params={"city_id": spb_id, "geojson": True} +).json() saint_data = read_df_from_geojson(geojson) m = folium.Map(