Remove custom sqlalchemy metadata, manage with __table_args__

Allow sqlmodel queries, with relations
Remode join_with mechanisms coming from gino
Handlew ith_only_columns in get_df and get_gdf
Implement feature-info
This commit is contained in:
phil 2024-01-04 18:50:23 +05:30
parent 1e3678fb69
commit ec71b6ed15
18 changed files with 353 additions and 141 deletions

View file

@ -22,7 +22,12 @@ from gisaf.security import (
Token, Token,
authenticate_user, get_current_user, create_access_token, authenticate_user, get_current_user, create_access_token,
) )
from gisaf.registry import registry from gisaf.registry import registry, NotInRegistry
from gisaf.redis_tools import store as redis_store
from gisaf.custom_store_base import BaseStore
from gisaf.models.to_migrate import (
FeatureInfo, InfoItem, Attachment, InfoCategory
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -43,7 +48,6 @@ async def bootstrap(
@api.post("/token") @api.post("/token")
async def login_for_access_token( async def login_for_access_token(
db_session: db_session,
form_data: OAuth2PasswordRequestForm = Depends() form_data: OAuth2PasswordRequestForm = Depends()
) -> Token: ) -> Token:
user = await authenticate_user(form_data.username, form_data.password) user = await authenticate_user(form_data.username, form_data.password)
@ -117,6 +121,121 @@ async def get_projects(
df = await db_session.run_sync(pandas_query, query) df = await db_session.run_sync(pandas_query, query)
return df.to_dict(orient="records") return df.to_dict(orient="records")
@api.get("/feature-info/{store}/{id}")
async def get_feature_info(
store: str, id: str,
) -> FeatureInfo:
store_record = registry.stores.loc[store]
model = store_record.model
if store_record.is_live:
item = await redis_store.get_feature_info(store, id)
geom = item.geometry
## Reproject to default coordinate system (WGS84),
## XXX: only for shapely geometries
if not isinstance(geom, pygeos.Geometry):
geom_reprojected = transform(reproject_func, geom)
geoInfoItems = OrderedDict()
if isinstance(geom, Point):
geoInfoItems['longitude'] = f'{geom.x:.6f}'
geoInfoItems['latitude'] = f'{geom.y:.6f}'
if geom.has_z:
geoInfoItems['elevation (m)'] = f'{geom.z:.6f}'
elif isinstance(geom, (LineString, MultiLineString)):
bounds = geom.bounds
geoInfoItems['longitude'] = f'{bounds[0]:.6f} - {bounds[2]:.6f}'
geoInfoItems['latitude'] = f'{bounds[1]:.6f} - {bounds[3]:.6f}'
geoInfoItems['length (m)'] = f'{geom_reprojected.length:.2f}'
## TODO: elevation for MultiLineString
if geom.has_z and not isinstance(geom, MultiLineString):
elevations = [cc[2] for cc in geom.coords]
elev_min = min(elevations)
elev_max = max(elevations)
if elev_min == elev_max:
geoInfoItems['elevation (m)'] = f'{elev_min:.2f}'
else:
geoInfoItems['elevation (m)'] = f'{elev_min:.2f} - {elev_max:.2f}'
elif isinstance(geom, (Polygon, MultiPolygon)):
area = geom_reprojected.area
bounds = geom.bounds
geoInfoItems['longitude'] = f'{bounds[0]:.6f} - {bounds[2]:.6f}'
geoInfoItems['latitude'] = f'{bounds[1]:.6f} - {bounds[3]:.6f}'
geoInfoItems['area (sq. m)'] = f'{area:.1f} sq. m'
geoInfoItems['area (ha)'] = f'{area / 10000:.1f} ha'
geoInfoItems['area (acre)'] = f'{area / 4046.85643005078874:.1f} acres'
## TODO: elevation for MultiPolygon
if geom.has_z and not isinstance(geom, MultiPolygon):
if hasattr(geom, 'exterior'):
coords = geom.exterior.coords
else:
coords = geom.coords
elevations = [coord[2] for coord in coords]
elev_min = min(elevations)
elev_max = max(elevations)
if elev_min == elev_max:
geoInfoItems['elevation (m)'] = f'{elev_min:.2f}'
else:
geoInfoItems['elevation (m)'] = f'{elev_min:.2f} - {elev_max:.2f}'
feature_info_dict = {
'itemName': item.get('popup', f'Live: {store} #{id}'),
'geoInfoItems': geoInfoItems,
'surveyInfoItems': {
'Note': 'Live layers do not have survey info',
},
'infoItems': dict(item.drop(set(item.keys()).intersection(('geometry', 'popup')))),
'tags': {},
}
elif issubclass(model, BaseStore):
feature_info_dict = await model.get_item_params(id)
else:
## Not a live layer
try:
feature_info_dict = await registry.get_model_id_params(model, int(id))
except NotInRegistry:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
feature_info = FeatureInfo(
id=id,
itemName=feature_info_dict['itemName'],
geoInfoItems=[InfoItem(key=k, value=v)
for k, v in feature_info_dict['geoInfoItems'].items()],
surveyInfoItems=[InfoItem(key=k, value=v)
for k, v in feature_info_dict['surveyInfoItems'].items()],
infoItems=[InfoItem(key=k, value=v)
for k, v in feature_info_dict['infoItems'].items()],
tags=[InfoItem(key=k, value=v)
for k, v in feature_info_dict['tags'].items()],
graph=feature_info_dict.get('graph'),
)
if 'files' in feature_info_dict and feature_info_dict['files'] is not None:
feature_info.files = [
Attachment(name=k, path=v)
for k, v in feature_info_dict['files'].items()
]
else:
feature_info.files = []
if 'images' in feature_info_dict and feature_info_dict['images'] is not None:
feature_info.images = [
Attachment(name=k, path=v)
for k, v in feature_info_dict['images'].items()
]
else:
feature_info.images = []
if 'categorized_info_items' in feature_info_dict and feature_info_dict['categorized_info_items'] != None:
feature_info.categorizedInfoItems = [
InfoCategory(
name=name,
infoItems=[
InfoItem(key=k, value=v)
for k, v in info_items.items()
]
)
for name, info_items in feature_info_dict['categorized_info_items'].items()
]
feature_info.externalRecordUrl = feature_info_dict.get('externalRecordUrl')
return feature_info
# @api.get("/user-role") # @api.get("/user-role")
# async def get_user_role_relation( # async def get_user_role_relation(
# *, db_session: AsyncSession = Depends(get_db_session) # *, db_session: AsyncSession = Depends(get_db_session)

View file

@ -40,17 +40,29 @@ class BaseModel(SQLModel):
return [] return []
@classmethod @classmethod
async def get_df(cls, where=None, with_related=True, **kwargs) -> pd.DataFrame: async def get_df(cls, *,
where=None, with_related=True, **kwargs
) -> pd.DataFrame:
return await cls._get_df(pandas_query, where=None, with_related=True, **kwargs) return await cls._get_df(pandas_query, where=None, with_related=True, **kwargs)
@classmethod @classmethod
async def get_gdf(cls, *, where=None, with_related=True, **kwargs) -> gpd.GeoDataFrame: async def get_gdf(cls, *,
return await cls._get_df(geopandas_query, where=None, with_related=True, **kwargs) where=None, with_related=True, **kwargs
) -> gpd.GeoDataFrame:
return await cls._get_df(geopandas_query,
where=None, with_related=True, **kwargs)
@classmethod @classmethod
async def _get_df(cls, method, *, where=None, with_related=True, **kwargs) -> pd.DataFrame | gpd.GeoDataFrame: async def _get_df(cls, method, *,
where=None, with_related=True, with_only_columns=[], **kwargs
) -> pd.DataFrame | gpd.GeoDataFrame:
async with db_session() as session: async with db_session() as session:
query = select(cls) if len(with_only_columns) == 0:
query = select(cls)
else:
columns = set(with_only_columns)
columns.add(*(col.name for col in cls.__table__.primary_key.columns))
query = select(*(getattr(cls, col) for col in columns))
if where is not None: if where is not None:
query.append_whereclause(where) query.append_whereclause(where)
## Get the joined tables ## Get the joined tables

View file

@ -9,7 +9,7 @@ import pandas as pd
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.survey import Surveyor, Equipment from gisaf.models.survey import Surveyor, Equipment
from gisaf.models.project import Project from gisaf.models.project import Project
from gisaf.models.metadata import gisaf_admin from gisaf.models.metadata import gisaf_admin_table_args
re_file_import_record_date_expr = '^(\S+)-(\d\d\d\d)-(\d\d)-(\d\d).*$' re_file_import_record_date_expr = '^(\S+)-(\d\d\d\d)-(\d\d)-(\d\d).*$'
@ -45,7 +45,7 @@ class FileImport(Model):
Give either url or path. Give either url or path.
""" """
__tablename__ = 'file_import' __tablename__ = 'file_import'
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
url: str url: str
@ -117,7 +117,7 @@ class FeatureImportData(Model):
Keep track of imported data, typically from shapefiles Keep track of imported data, typically from shapefiles
""" """
__tablename__ = 'feature_import_data' __tablename__ = 'feature_import_data'
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
store: str = Field(index=True) store: str = Field(index=True)

View file

@ -1,15 +1,20 @@
from sqlmodel import Field, SQLModel, MetaData, Relationship from sqlmodel import Field, SQLModel, Relationship
from gisaf.models.metadata import gisaf_admin_table_args
from gisaf.models.metadata import gisaf_admin
class UserRoleLink(SQLModel, table=True): class UserRoleLink(SQLModel, table=True):
metadata = gisaf_admin
__tablename__ = 'roles_users' __tablename__ = 'roles_users'
__table_args__ = gisaf_admin_table_args
user_id: int | None = Field( user_id: int | None = Field(
default=None, foreign_key="user.id", primary_key=True default=None,
foreign_key=gisaf_admin_table_args['schema'] + '.user.id',
primary_key=True
) )
role_id: int | None = Field( role_id: int | None = Field(
default=None, foreign_key="role.id", primary_key=True default=None,
foreign_key=gisaf_admin_table_args['schema'] + '.role.id',
primary_key=True
) )
@ -20,7 +25,7 @@ class UserBase(SQLModel):
class User(UserBase, table=True): class User(UserBase, table=True):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
roles: list["Role"] = Relationship(back_populates="users", roles: list["Role"] = Relationship(back_populates="users",
link_model=UserRoleLink) link_model=UserRoleLink)
@ -40,7 +45,7 @@ class RoleWithDescription(RoleBase):
description: str | None description: str | None
class Role(RoleWithDescription, table=True): class Role(RoleWithDescription, table=True):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
users: list[User] = Relationship(back_populates="roles", users: list[User] = Relationship(back_populates="roles",
link_model=UserRoleLink) link_model=UserRoleLink)

View file

@ -4,8 +4,8 @@ from sqlalchemy import String
from pydantic import computed_field, ConfigDict from pydantic import computed_field, ConfigDict
from sqlmodel import Field, Relationship, JSON, TEXT from sqlmodel import Field, Relationship, JSON, TEXT
from gisaf.models.metadata import gisaf_survey
from gisaf.database import BaseModel from gisaf.database import BaseModel
from gisaf.models.metadata import gisaf_survey_table_args
mapbox_type_mapping = { mapbox_type_mapping = {
'Point': 'symbol', 'Point': 'symbol',
@ -15,8 +15,8 @@ mapbox_type_mapping = {
class CategoryGroup(BaseModel, table=True): class CategoryGroup(BaseModel, table=True):
metadata = gisaf_survey
__tablename__ = 'category_group' __tablename__ = 'category_group'
__table_args__ = gisaf_survey_table_args
name: str | None = Field(sa_type=String(4), default=None, primary_key=True) name: str | None = Field(sa_type=String(4), default=None, primary_key=True)
major: str major: str
long_name: str long_name: str
@ -28,8 +28,8 @@ class CategoryGroup(BaseModel, table=True):
class CategoryModelType(BaseModel, table=True): class CategoryModelType(BaseModel, table=True):
metadata = gisaf_survey
__tablename__ = 'category_model_type' __tablename__ = 'category_model_type'
__table_args__ = gisaf_survey_table_args
name: str | None = Field(default=None, primary_key=True) name: str | None = Field(default=None, primary_key=True)
class Admin: class Admin:
@ -47,14 +47,15 @@ class CategoryBase(BaseModel):
domain: ClassVar[str] = 'V' domain: ClassVar[str] = 'V'
description: str | None description: str | None
group: str = Field(sa_type=String(4), group: str = Field(sa_type=String(4),
foreign_key="category_group.name", index=True) foreign_key=gisaf_survey_table_args['schema'] + ".category_group.name",
index=True)
minor_group_1: str = Field(sa_type=String(4), default='----') minor_group_1: str = Field(sa_type=String(4), default='----')
minor_group_2: str = Field(sa_type=String(4), default='----') minor_group_2: str = Field(sa_type=String(4), default='----')
status: str = Field(sa_type=String(1)) status: str = Field(sa_type=String(1))
custom: bool | None custom: bool | None
auto_import: bool = True auto_import: bool = True
gis_type: str = Field(sa_type=String(50), gis_type: str = Field(sa_type=String(50),
foreign_key='category_model_type.name', foreign_key=gisaf_survey_table_args['schema'] + '.category_model_type.name',
default='Point') default='Point')
long_name: str | None = Field(sa_type=String(50)) long_name: str | None = Field(sa_type=String(50))
style: str | None = Field(sa_type=TEXT) style: str | None = Field(sa_type=TEXT)
@ -105,7 +106,7 @@ class CategoryBase(BaseModel):
class Category(CategoryBase, table=True): class Category(CategoryBase, table=True):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
name: str | None = Field(default=None, primary_key=True) name: str | None = Field(default=None, primary_key=True)
category_group: CategoryGroup = Relationship(back_populates="categories") category_group: CategoryGroup = Relationship(back_populates="categories")

View file

@ -4,6 +4,7 @@ from datetime import date, datetime
from collections import OrderedDict from collections import OrderedDict
from io import BytesIO from io import BytesIO
from zipfile import ZipFile from zipfile import ZipFile
from functools import cached_property
import locale import locale
import logging import logging
@ -12,19 +13,16 @@ import geopandas as gpd # type: ignore
import shapely # type: ignore import shapely # type: ignore
import pyproj import pyproj
from sqlmodel import SQLModel, Field, Relationship
from sqlmodel.ext.asyncio.session import AsyncSession
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import select, Field, Relationship
from geoalchemy2.shape import from_shape from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.dialects.postgresql import BIGINT from sqlalchemy import BigInteger, MetaData, String, func, and_, text
from sqlalchemy import BigInteger, Column, MetaData, String, func, and_, text
from sqlalchemy.sql import sqltypes from sqlalchemy.sql import sqltypes
from sqlalchemy.orm import declared_attr
from psycopg2.extensions import adapt from psycopg2.extensions import adapt
from geoalchemy2.shape import from_shape
from geoalchemy2.types import Geometry, WKBElement from geoalchemy2.types import Geometry, WKBElement
from shapely import wkb, from_wkb
from shapely import wkb
from shapely.geometry import mapping from shapely.geometry import mapping
from shapely.ops import transform # type: ignore from shapely.ops import transform # type: ignore
@ -35,15 +33,15 @@ from shapefile import (
POLYGON, POLYGONZ, POLYGON, POLYGONZ,
) )
from gisaf.database import db_session from gisaf.database import db_session
from gisaf.config import conf from gisaf.config import conf
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import survey, raw_survey from gisaf.models.metadata import (
from gisaf.models.survey import Equipment, Surveyor, Accuracy gisaf_survey_table_args, gisaf_admin_table_args, survey_table_args)
from gisaf.models.misc import Qml from gisaf.models.misc import Qml
from gisaf.models.category import Category from gisaf.models.category import Category
from gisaf.models.project import Project # from gisaf.models.survey import Equipment, Surveyor, Accuracy
# from gisaf.models.project import Project
LOCALE_DATE_FORMAT = locale.nl_langinfo(locale.D_FMT) LOCALE_DATE_FORMAT = locale.nl_langinfo(locale.D_FMT)
@ -82,14 +80,14 @@ class BaseSurveyModel(BaseModel):
- projected ('V_*') - projected ('V_*')
""" """
id: int | None = Field(sa_type=BigInteger, primary_key=True, default=None) id: int | None = Field(sa_type=BigInteger, primary_key=True, default=None)
equip_id: int = Field(foreign_key='equipment.id') equip_id: int = Field(
# equipment: Equipment = Relationship() foreign_key=gisaf_survey_table_args['schema'] + '.equipment.id')
srvyr_id: int = Field('surveyor.id') srvyr_id: int = Field(
# surveyor: Surveyor = Relationship() foreign_key=gisaf_survey_table_args['schema'] + '.surveyor.id')
accur_id: int = Field('accuracy.id') accur_id: int = Field(
# accuracy: Accuracy = Relationship() foreign_key=gisaf_survey_table_args['schema'] + '.accuracy.id')
project_id: int = Field('project.id') project_id: int = Field(
# project: Project = Relationship() foreign_key=gisaf_admin_table_args['schema'] + '.project.id')
orig_id: str orig_id: str
date: date date: date
@ -151,7 +149,7 @@ class SurveyModel(BaseSurveyModel):
""" """
Base mixin class for defining final (reprojected) survey data, with a status Base mixin class for defining final (reprojected) survey data, with a status
""" """
metadata: ClassVar[MetaData] = survey __table_args__ = survey_table_args
# status: str = Field(sa_type=String(1)) # status: str = Field(sa_type=String(1))
get_gdf_with_related: ClassVar[bool] = False get_gdf_with_related: ClassVar[bool] = False
@ -182,6 +180,10 @@ class SurveyModel(BaseSurveyModel):
'gisaf_admin_project_skip_columns', 'gisaf_admin_project_skip_columns',
] ]
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__ # type: nocheck
async def get_survey_info(self): async def get_survey_info(self):
info = await super(SurveyModel, self).get_survey_info() info = await super(SurveyModel, self).get_survey_info()
if self.srvyr_id: if self.srvyr_id:
@ -203,7 +205,7 @@ class SurveyModel(BaseSurveyModel):
@property @property
def caption(self): def caption(self):
return '{self.category.description} [{self.category.group}-{self.category.minor_group_1}] #{self.id:d}'.format(self=self) return '{self.category.description} - {self.category.name} [{self.category.group}-{self.category.minor_group_1}] #{self.id:d}'.format(self=self)
@classmethod @classmethod
async def get_popup(cls, df): async def get_popup(cls, df):
@ -425,30 +427,28 @@ class GeoModelNoStatus(Model):
async def get_tags(self): async def get_tags(self):
from gisaf.models.tags import Tags from gisaf.models.tags import Tags
tags = await Tags.get_df( async with db_session() as session:
where=and_( query = select(Tags.tags).where(Tags.store == self.get_store_name(),
Tags.store == self.__class__.get_store_name(), Tags.ref_id == self.id)
Tags.ref_id == self.id tags = await session.exec(query)
), return tags.one_or_none() or {}
with_only_columns=['tags']
)
if len(tags) > 0:
return tags.loc[0, 'tags']
else:
return {}
@property @cached_property
def shapely_geom(self): def shapely_geom(self):
if not hasattr(self, '_shapely_geom'): return from_wkb(self.geom.data)
if isinstance(self.geom, WKBElement):
bytes = self.geom.data # @property
if bytes: # def shapely_geom(self):
self._shapely_geom = wkb.loads(bytes) # if not hasattr(self, '_shapely_geom'):
else: # if isinstance(self.geom, WKBElement):
self._shapely_geom = None # bytes = self.geom.data
else: # if bytes:
self._shapely_geom = None # self._shapely_geom = wkb.loads(bytes)
return self._shapely_geom # else:
# self._shapely_geom = None
# else:
# self._shapely_geom = None
# return self._shapely_geom
def get_bgColor(self): def get_bgColor(self):
""" """
@ -760,6 +760,7 @@ class GeoModelNoStatus(Model):
def get_attachment_base_dir(cls): def get_attachment_base_dir(cls):
return Path(conf.attachments.base_dir) / cls.get_attachment_dir() return Path(conf.attachments.base_dir) / cls.get_attachment_dir()
class GeoModel(GeoModelNoStatus): class GeoModel(GeoModelNoStatus):
status: ClassVar[str] = 'E' status: ClassVar[str] = 'E'
""" """
@ -1061,7 +1062,7 @@ class RawSurveyBaseModel(BaseSurveyModel, GeoPointModelNoStatus):
""" """
Abstract base class for category based raw survey point models Abstract base class for category based raw survey point models
""" """
metadata: ClassVar[MetaData] = raw_survey # metadata: ClassVar[MetaData] = raw_survey
geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3, geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3,
srid=conf.geo.raw_survey.srid)) srid=conf.geo.raw_survey.srid))
status: str = Field(sa_type=String(1)) status: str = Field(sa_type=String(1))

View file

@ -3,11 +3,11 @@ from typing import Any
from sqlmodel import Field, String, JSON, Relationship from sqlmodel import Field, String, JSON, Relationship
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_map from gisaf.models.metadata import gisaf_map_table_args
class BaseStyle(Model): class BaseStyle(Model):
metadata = gisaf_map __table_args__ = gisaf_map_table_args
__tablename__ = 'map_base_style' __tablename__ = 'map_base_style'
class Admin: class Admin:
@ -26,7 +26,7 @@ class BaseStyle(Model):
class BaseMap(Model): class BaseMap(Model):
metadata = gisaf_map __table_args__ = gisaf_map_table_args
__tablename__ = 'base_map' __tablename__ = 'base_map'
class Admin: class Admin:
@ -43,7 +43,7 @@ class BaseMap(Model):
class BaseMapLayer(Model): class BaseMapLayer(Model):
metadata = gisaf_map __table_args__ = gisaf_map_table_args
__tablename__ = 'base_map_layer' __tablename__ = 'base_map_layer'
class Admin: class Admin:

View file

@ -1,10 +1,8 @@
from sqlmodel import MetaData
from gisaf.config import conf from gisaf.config import conf
gisaf = MetaData(schema='gisaf') gisaf_table_args = dict(schema= 'gisaf')
gisaf_survey = MetaData(schema='gisaf_survey') gisaf_survey_table_args = dict(schema='gisaf_survey')
gisaf_admin = MetaData(schema='gisaf_admin') gisaf_admin_table_args = dict(schema='gisaf_admin')
gisaf_map = MetaData(schema='gisaf_map') gisaf_map_table_args = dict(schema='gisaf_map')
raw_survey = MetaData(schema=conf.survey.db_schema_raw) raw_survey_table_args = dict(schema=conf.survey.db_schema_raw)
survey = MetaData(schema=conf.survey.db_schema) survey_table_args = dict(schema=conf.survey.db_schema)

View file

@ -5,7 +5,7 @@ from pydantic import ConfigDict
from sqlmodel import Field, JSON, Column from sqlmodel import Field, JSON, Column
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_map from gisaf.models.metadata import gisaf_map_table_args
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,7 +19,7 @@ class Qml(Model):
Model for storing qml (QGis style) Model for storing qml (QGis style)
""" """
model_config = ConfigDict(protected_namespaces=()) model_config = ConfigDict(protected_namespaces=())
metadata = gisaf_map __table_args__ = gisaf_map_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'

View file

@ -38,7 +38,7 @@ class Model(BaseModel):
if hasattr(cls, '__table__'): if hasattr(cls, '__table__'):
return cls.__table__.fullname return cls.__table__.fullname
elif hasattr(cls, '__table_args__') and 'schema' in cls.__table_args__: elif hasattr(cls, '__table_args__') and 'schema' in cls.__table_args__:
return f"{cls.__table_args__.schema}.{cls.__tablename__}" return f"{cls.__table_args__['schema']}.{cls.__tablename__}"
else: else:
return f'{cls.metadata.schema}.{cls.__tablename__}' return f'{cls.metadata.schema}.{cls.__tablename__}'

View file

@ -9,10 +9,10 @@ from shapely.geometry import Point
from gisaf.config import conf from gisaf.config import conf
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_admin from gisaf.models.metadata import gisaf_admin_table_args
class Project(Model, table=True): class Project(Model, table=True):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'

View file

@ -5,10 +5,10 @@ from gisaf.models.models_base import Model
from gisaf.models.geo_models_base import GeoPointMModel, BaseSurveyModel from gisaf.models.geo_models_base import GeoPointMModel, BaseSurveyModel
from gisaf.models.project import Project from gisaf.models.project import Project
from gisaf.models.category import Category from gisaf.models.category import Category
from gisaf.models.metadata import gisaf_survey from gisaf.models.metadata import gisaf_survey_table_args
class RawSurveyModel(BaseSurveyModel, GeoPointMModel): class RawSurveyModel(BaseSurveyModel, GeoPointMModel):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
__tablename__ = 'raw_survey' __tablename__ = 'raw_survey'
hidden: ClassVar[bool] = True hidden: ClassVar[bool] = True
@ -94,7 +94,7 @@ class OriginRawPoint(Model):
for each line and polygon shape for each line and polygon shape
Filled when importing shapefiles Filled when importing shapefiles
""" """
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
__tablename__ = 'origin_raw_point' __tablename__ = 'origin_raw_point'
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)

View file

@ -4,11 +4,11 @@ from sqlalchemy import BigInteger, String
from sqlmodel import Field from sqlmodel import Field
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_admin from gisaf.models.metadata import gisaf_admin_table_args
class Reconciliation(Model): class Reconciliation(Model):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'
@ -21,7 +21,7 @@ class Reconciliation(Model):
class StatusChange(Model): class StatusChange(Model):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
__tablename__ = 'status_change' __tablename__ = 'status_change'
id: int = Field(primary_key=True, sa_type=BigInteger, id: int = Field(primary_key=True, sa_type=BigInteger,
@ -34,7 +34,7 @@ class StatusChange(Model):
class FeatureDeletion(Model): class FeatureDeletion(Model):
metadata = gisaf_admin __table_args__ = gisaf_admin_table_args
__tablename__ = 'feature_deletion' __tablename__ = 'feature_deletion'
id: int = Field(BigInteger, primary_key=True, id: int = Field(BigInteger, primary_key=True,

View file

@ -3,11 +3,11 @@ from enum import Enum
from sqlmodel import Field, Relationship from sqlmodel import Field, Relationship
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_survey from gisaf.models.metadata import gisaf_survey_table_args
class Accuracy(Model, table=True): class Accuracy(Model, table=True):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'
@ -25,7 +25,7 @@ class Accuracy(Model, table=True):
class Surveyor(Model, table=True): class Surveyor(Model, table=True):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'
@ -42,7 +42,7 @@ class Surveyor(Model, table=True):
class Equipment(Model, table=True): class Equipment(Model, table=True):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
class Admin: class Admin:
menu = 'Other' menu = 'Other'
@ -62,17 +62,17 @@ class GeometryType(str, Enum):
line_work = 'Line_work' line_work = 'Line_work'
class AccuracyEquimentSurveyorMapping(Model, table=True): class AccuracyEquimentSurveyorMapping(Model, table=True):
metadata = gisaf_survey __table_args__ = gisaf_survey_table_args
__tablename__ = 'accuracy_equiment_surveyor_mapping' __tablename__ = 'accuracy_equiment_surveyor_mapping'
class Admin: class Admin:
menu = 'Other' menu = 'Other'
id: int | None= Field(default=None, primary_key=True) id: int | None= Field(default=None, primary_key=True)
surveyor_id: int = Field(foreign_key='surveyor.id', index=True) surveyor_id: int = Field(foreign_key=gisaf_survey_table_args['schema'] + '.surveyor.id', index=True)
equipment_id: int = Field(foreign_key='equipment.id', index=True) equipment_id: int = Field(foreign_key=gisaf_survey_table_args['schema'] + '.equipment.id', index=True)
geometry_type: GeometryType = Field(default='Point', index=True) geometry_type: GeometryType = Field(default='Point', index=True)
accuracy_id: int = Field(foreign_key='accuracy.id') accuracy_id: int = Field(foreign_key=gisaf_survey_table_args['schema'] + '.accuracy.id')
surveyor: Surveyor = Relationship() surveyor: Surveyor = Relationship()
accuracy: Accuracy = Relationship() accuracy: Accuracy = Relationship()
equipment: Equipment = Relationship() equipment: Equipment = Relationship()

View file

@ -5,11 +5,11 @@ from sqlalchemy.dialects.postgresql import HSTORE
from sqlmodel import Field, SQLModel, MetaData, JSON, TEXT, Relationship, Column from sqlmodel import Field, SQLModel, MetaData, JSON, TEXT, Relationship, Column
from pydantic import computed_field from pydantic import computed_field
from gisaf.models.metadata import gisaf from gisaf.models.metadata import gisaf_table_args
from gisaf.models.geo_models_base import GeoPointModel from gisaf.models.geo_models_base import GeoPointModel
class Tags(GeoPointModel, table=True): class Tags(GeoPointModel, table=True):
metadata = gisaf __table_args__ = gisaf_table_args
hidden: ClassVar[bool] = True hidden: ClassVar[bool] = True
class Admin: class Admin:
@ -29,7 +29,7 @@ class Tags(GeoPointModel, table=True):
class TagKey(SQLModel, table=True): class TagKey(SQLModel, table=True):
metadata = gisaf __table_args__ = gisaf_table_args
## CREATE TABLE gisaf.tagkey (key VARCHAR(255) primary key); ## CREATE TABLE gisaf.tagkey (key VARCHAR(255) primary key);
class Admin: class Admin:

View file

@ -3,20 +3,81 @@ from pydantic import BaseModel
class ActionResult(BaseModel): class ActionResult(BaseModel):
message: str message: str
class ActionResults(BaseModel): class ActionResults(BaseModel):
name: str name: str
message: str message: str
actionResults: list[ActionResult] actionResults: list[ActionResult]
class FormField(BaseModel): class FormField(BaseModel):
name: str name: str
type: str type: str
class ModelAction(BaseModel): class ModelAction(BaseModel):
name: str name: str
icon: str icon: str
formFields: list[FormField] formFields: list[FormField]
class DataProvider(BaseModel): class DataProvider(BaseModel):
name: str name: str
values: list[str] values: list[str]
class InfoItem(BaseModel):
key: str
value: str
class InfoCategory(BaseModel):
name: str
infoItems: list[InfoItem]
class PlotBgShape(BaseModel):
name: str
valueTop: float
valueBottom: float
color: str
class PlotBaseLine(BaseModel):
name: str
value: float
color: str
class PlotParams(BaseModel):
baseLines: list[PlotBaseLine]
bgShape: list[PlotBgShape]
barBase: float
class Attachment(BaseModel):
name: str
path: str
class FeatureInfo(BaseModel):
id: str
itemName: str
geoInfoItems: list[InfoItem] = []
surveyInfoItems: list[InfoItem] = []
infoItems: list[InfoItem] = []
categorizedInfoItems: list[InfoCategory] = []
tags: list[InfoItem] = []
graph: str | None = None
plotParams: PlotParams | None = None
files: list[Attachment] = []
images: list[Attachment] = []
externalRecordUrl: str | None = None
class MapboxPaint(BaseModel):
...
class MapboxLayout(BaseModel):
...

View file

@ -9,10 +9,10 @@ from importlib.metadata import entry_points
from typing import Any, ClassVar, Literal from typing import Any, ClassVar, Literal
from pydantic import create_model from pydantic import create_model
from pydantic_core import PydanticUndefined
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload, joinedload
from sqlmodel import SQLModel, select, inspect from sqlalchemy.exc import NoResultFound
from sqlmodel import SQLModel, select, inspect, Relationship
import pandas as pd import pandas as pd
import numpy as np import numpy as np
@ -30,11 +30,14 @@ from gisaf.models.geo_models_base import (
GeoLineSurveyModel, GeoLineSurveyModel,
GeoPolygonSurveyModel, GeoPolygonSurveyModel,
) )
from gisaf.models.survey import Equipment, Surveyor, Accuracy
from gisaf.models.project import Project
from gisaf.utils import ToMigrate from gisaf.utils import ToMigrate
from gisaf.models.category import Category, CategoryGroup from gisaf.models.category import Category, CategoryGroup
from gisaf.database import db_session from gisaf.database import db_session
from gisaf import models from gisaf import models
from gisaf.models.metadata import survey, raw_survey from gisaf.models.metadata import (
gisaf_survey_table_args, raw_survey_table_args, survey_table_args)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -139,8 +142,8 @@ class ModelRegistry:
## Use pydantic create_model, supported by SQLModel ## Use pydantic create_model, supported by SQLModel
## See https://github.com/tiangolo/sqlmodel/issues/377 ## See https://github.com/tiangolo/sqlmodel/issues/377
store_name = f'{survey.schema}.{category.table_name}' store_name = f"{survey_table_args['schema']}.{category.table_name}"
raw_store_name = f'{raw_survey.schema}.RAW_{category.table_name}' raw_store_name = f"{raw_survey_table_args['schema']}.RAW_{category.table_name}"
raw_survey_field_definitions = { raw_survey_field_definitions = {
## FIXME: RawSurveyBaseModel.category should be a Category, not category.name ## FIXME: RawSurveyBaseModel.category should be a Category, not category.name
'category_name': (ClassVar[str], category.name), 'category_name': (ClassVar[str], category.name),
@ -157,7 +160,6 @@ class ModelRegistry:
__model_name=category.raw_survey_table_name, __model_name=category.raw_survey_table_name,
__cls_kwargs__={ __cls_kwargs__={
'table': True, 'table': True,
'__tablename__': category.raw_survey_table_name,
}, },
**raw_survey_field_definitions **raw_survey_field_definitions
) )
@ -173,10 +175,15 @@ class ModelRegistry:
if model_class: if model_class:
survey_field_definitions = { survey_field_definitions = {
'category_name': (ClassVar[str], category.name), 'category_name': (ClassVar[str], category.name),
'category': (ClassVar[Category], category),
'group_name': (ClassVar[str], category.category_group.name), 'group_name': (ClassVar[str], category.category_group.name),
'raw_store_name': (ClassVar[str], raw_store_name), 'raw_store_name': (ClassVar[str], raw_store_name),
'viewable_role': (ClassVar[str], category.viewable_role), 'viewable_role': (ClassVar[str], category.viewable_role),
'symbol': (ClassVar[str], category.symbol), 'symbol': (ClassVar[str], category.symbol),
'equipment': (Equipment, Relationship()),
'surveyor': (Surveyor, Relationship()),
'accuracy': (Accuracy, Relationship()),
'project': (Project, Relationship()),
#'raw_model': (str, self.raw_survey_models.get(raw_store_name)), #'raw_model': (str, self.raw_survey_models.get(raw_store_name)),
# 'icon': (str, f'{survey.schema}-{category.table_name}'), # 'icon': (str, f'{survey.schema}-{category.table_name}'),
} }
@ -185,7 +192,6 @@ class ModelRegistry:
__model_name=category.table_name, __model_name=category.table_name,
__cls_kwargs__={ __cls_kwargs__={
'table': True, 'table': True,
'__tablename__': category.table_name,
}, },
**survey_field_definitions, **survey_field_definitions,
) )
@ -335,7 +341,16 @@ class ModelRegistry:
""" """
if not model: if not model:
return {} return {}
item = await model.load(**model.get_join_with()).query.where(model.id==id).gino.first() async with db_session() as session:
query = select(model).where(model.id == id).options(
*(joinedload(jt) for jt in model.selectinload()))
result = await session.exec(query)
try:
item = result.one()
except NoResultFound:
raise NotInRegistry
# item = await model.load(**model.get_join_with()).query.where(model.id==id).gino.first()
if not item: if not item:
return {} return {}
resp = {} resp = {}
@ -386,7 +401,7 @@ class ModelRegistry:
if category.minor_group_2 != '----': if category.minor_group_2 != '----':
fragments.append(category.minor_group_2) fragments.append(category.minor_group_2)
return '.'.join([ return '.'.join([
survey.schema, survey_table_args['schema'],
'_'.join(fragments) '_'.join(fragments)
]) ])

View file

@ -126,32 +126,32 @@ class NumpyEncoder(JSONEncoder):
# headers=headers, content_type=content_type, **kwargs) # headers=headers, content_type=content_type, **kwargs)
def get_join_with(cls, recursive=True): # def get_join_with(cls, recursive=True):
""" # """
Helper function for loading related tables with a Gino loader (left outer join) # Helper function for loading related tables with a Gino loader (left outer join)
Should work recursively... # Should work recursively...
Eg: # Eg:
cls.load(**get_join_with(cls)).query.gino.all() # cls.load(**get_join_with(cls)).query.gino.all()
:param cls: # :param cls:
:return: # :return:
""" # """
if hasattr(cls, 'dyn_join_with'): # if hasattr(cls, 'dyn_join_with'):
joins = cls.dyn_join_with() # joins = cls.dyn_join_with()
else: # else:
joins = {} # joins = {}
if hasattr(cls, '_join_with'): # if hasattr(cls, '_join_with'):
joins.update(cls._join_with) # joins.update(cls._join_with)
if not recursive: # if not recursive:
return joins # return joins
recursive_joins = {} # recursive_joins = {}
for name, join in joins.items(): # for name, join in joins.items():
more_joins = get_join_with(join) # more_joins = get_join_with(join)
if more_joins: # if more_joins:
aliased = {name: join.alias() for name, join in more_joins.items()} # aliased = {name: join.alias() for name, join in more_joins.items()}
recursive_joins[name] = join.load(**aliased) # recursive_joins[name] = join.load(**aliased)
else: # else:
recursive_joins[name] = join # recursive_joins[name] = join
return recursive_joins # return recursive_joins
def get_joined_query(cls): def get_joined_query(cls):
""" """