Restructure api
Fixes in geo api, registry Cleanups
This commit is contained in:
parent
c84dd61f6a
commit
8c299f0041
17 changed files with 212 additions and 162 deletions
|
@ -1 +1 @@
|
||||||
__version__: str = '2023.4.dev34+g5dacc90.d20240212'
|
__version__: str = '2023.4.dev37+gb00bf1f.d20240226'
|
|
@ -1,6 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status, responses
|
from fastapi import Depends, APIRouter, HTTPException, status, responses
|
||||||
|
|
||||||
from gisaf.models.admin import AdminBasket, BasketNameOnly
|
from gisaf.models.admin import AdminBasket, BasketNameOnly
|
||||||
from gisaf.models.authentication import User
|
from gisaf.models.authentication import User
|
||||||
|
@ -9,8 +9,10 @@ from gisaf.admin import manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
api = FastAPI(
|
api = APIRouter(
|
||||||
default_response_class=responses.ORJSONResponse,
|
tags=["admin"],
|
||||||
|
# dependencies=[Depends(get_token_header)],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.get('/basket')
|
@api.get('/basket')
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status, responses
|
from fastapi import Depends, APIRouter, HTTPException, status, responses
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
@ -34,8 +34,10 @@ Gisaf is free, open source software for geomatics and GIS:
|
||||||
<a href="http://redmine.auroville.org.in/projects/gisaf">Gisaf</a>.
|
<a href="http://redmine.auroville.org.in/projects/gisaf">Gisaf</a>.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
api = FastAPI(
|
api = APIRouter(
|
||||||
default_response_class=responses.ORJSONResponse,
|
tags=["dashboard"],
|
||||||
|
# dependencies=[Depends(get_token_header)],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.get('/groups')
|
@api.get('/groups')
|
||||||
|
|
|
@ -7,7 +7,7 @@ import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
|
|
||||||
from fastapi import (Depends, FastAPI, HTTPException, Response, Header,
|
from fastapi import (Depends, APIRouter, HTTPException, Response, Header,
|
||||||
WebSocket, WebSocketDisconnect,
|
WebSocket, WebSocketDisconnect,
|
||||||
status, responses)
|
status, responses)
|
||||||
|
|
||||||
|
@ -20,8 +20,10 @@ from gisaf.security import get_current_active_user
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
api = FastAPI(
|
api = APIRouter(
|
||||||
default_response_class=responses.ORJSONResponse,
|
tags=["geoapi"],
|
||||||
|
# dependencies=[Depends(get_token_header)],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
|
@ -103,11 +105,10 @@ async def get_geojson(store_name,
|
||||||
if model.cache_enabled:
|
if model.cache_enabled:
|
||||||
ttag = await redis_store.get_ttag(store_name)
|
ttag = await redis_store.get_ttag(store_name)
|
||||||
if ttag and If_None_Match == ttag:
|
if ttag and If_None_Match == ttag:
|
||||||
return status.HTTP_304_NOT_MODIFIED
|
raise HTTPException(status.HTTP_304_NOT_MODIFIED)
|
||||||
if hasattr(model, 'get_geojson'):
|
if hasattr(model, 'get_geojson'):
|
||||||
geojson = await model.get_geojson(simplify_tolerance=simplify,
|
geojson = await model.get_geojson(simplify_tolerance=simplify,
|
||||||
preserve_topology=preserveTopology,
|
preserve_topology=preserveTopology)
|
||||||
registry=registry)
|
|
||||||
## Store to redis for caching
|
## Store to redis for caching
|
||||||
if use_cache:
|
if use_cache:
|
||||||
await redis_store.store_json(model, geojson)
|
await redis_store.store_json(model, geojson)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status, responses
|
from fastapi import Depends, APIRouter, HTTPException, status, responses
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
@ -12,7 +12,6 @@ from gisaf.models.authentication import (
|
||||||
Role, RoleRead,
|
Role, RoleRead,
|
||||||
)
|
)
|
||||||
from gisaf.models.category import Category, CategoryRead
|
from gisaf.models.category import Category, CategoryRead
|
||||||
from gisaf.models.geo_models_base import LineWorkSurveyModel
|
|
||||||
from gisaf.models.to_migrate import DataProvider
|
from gisaf.models.to_migrate import DataProvider
|
||||||
from gisaf.models.survey import Equipment, SurveyMeta, Surveyor
|
from gisaf.models.survey import Equipment, SurveyMeta, Surveyor
|
||||||
from gisaf.config import Survey, conf
|
from gisaf.config import Survey, conf
|
||||||
|
@ -31,19 +30,16 @@ from gisaf.models.to_migrate import (
|
||||||
FeatureInfo, InfoItem, Attachment, InfoCategory
|
FeatureInfo, InfoItem, Attachment, InfoCategory
|
||||||
)
|
)
|
||||||
from gisaf.live_utils import get_live_feature_info
|
from gisaf.live_utils import get_live_feature_info
|
||||||
from gisaf.api.dashboard import api as dashboard_api
|
|
||||||
from gisaf.api.map import api as map_api
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
api = FastAPI(
|
api = APIRouter(
|
||||||
default_response_class=responses.ORJSONResponse,
|
tags=["api"],
|
||||||
|
# dependencies=[Depends(get_token_header)],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
#api.add_middleware(SessionMiddleware, secret_key=conf.crypto.secret)
|
#api.add_middleware(SessionMiddleware, secret_key=conf.crypto.secret)
|
||||||
api.mount('/dashboard', dashboard_api)
|
|
||||||
api.mount('/map', map_api)
|
|
||||||
|
|
||||||
|
|
||||||
@api.get('/bootstrap')
|
@api.get('/bootstrap')
|
||||||
async def bootstrap(
|
async def bootstrap(
|
|
@ -2,21 +2,26 @@ from collections import OrderedDict, defaultdict
|
||||||
import logging
|
import logging
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, HTTPException, status, responses
|
from fastapi import (APIRouter, Request, HTTPException,
|
||||||
|
Response, status)
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
from gisaf.models.map_bases import BaseMap, BaseMapLayer, BaseStyle, MapInitData
|
from gisaf.models.map_bases import (BaseMap, BaseMapLayer, BaseMapWithStores,
|
||||||
|
BaseStyle, MapInitData, MaplibreStyle)
|
||||||
|
from gisaf.models.misc import Qml
|
||||||
from gisaf.registry import registry
|
from gisaf.registry import registry
|
||||||
from gisaf.database import db_session
|
from gisaf.database import db_session, fastapi_db_session
|
||||||
from gisaf.database import fastapi_db_session
|
|
||||||
from gisaf.tiles import registry as tiles_registry
|
from gisaf.tiles import registry as tiles_registry
|
||||||
|
from gisaf.redis_tools import store as redis_store
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
api = FastAPI(
|
api = APIRouter(
|
||||||
default_response_class=responses.ORJSONResponse,
|
tags=["map"],
|
||||||
|
# dependencies=[Depends(get_token_header)],
|
||||||
|
# responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_base_styles():
|
async def get_base_styles():
|
||||||
|
@ -31,23 +36,21 @@ async def get_base_styles():
|
||||||
# base_styles.extend(tiles_registry.mbtiles.values())
|
# base_styles.extend(tiles_registry.mbtiles.values())
|
||||||
return [BaseStyle(name=bs) for bs in base_styles] # type: ignore
|
return [BaseStyle(name=bs) for bs in base_styles] # type: ignore
|
||||||
|
|
||||||
async def get_base_maps() -> list[BaseMap]:
|
async def get_base_maps() -> list[BaseMapWithStores]:
|
||||||
async with db_session() as session:
|
async with db_session() as session:
|
||||||
query1 = select(BaseMap).options(selectinload(BaseMap.layers)) # type: ignore
|
query1 = select(BaseMap) #.options(selectinload(BaseMap.layers)) # type: ignore
|
||||||
data1 = await session.exec(query1)
|
data1 = await session.exec(query1)
|
||||||
base_maps = data1.all()
|
base_maps = data1.all()
|
||||||
return base_maps # type: ignore
|
|
||||||
base_map_dict = {bm.id: bm.name for bm in base_maps}
|
base_map_dict = {bm.id: bm.name for bm in base_maps}
|
||||||
query2 = select(BaseMapLayer).options(selectinload(BaseMapLayer.base_map))
|
query2 = select(BaseMapLayer).options(selectinload(BaseMapLayer.base_map)) # type: ignore
|
||||||
data2 = await session.exec(query2)
|
data2 = await session.exec(query2)
|
||||||
base_map_layers = data2.all()
|
base_map_layers = data2.all()
|
||||||
bms = defaultdict(list)
|
bms: dict[str, list] = defaultdict(list)
|
||||||
for bml in base_map_layers:
|
for bml in base_map_layers:
|
||||||
breakpoint()
|
|
||||||
if bml.store:
|
if bml.store:
|
||||||
bms[base_map_dict[bml.base_map_id]].append(name=bml.store)
|
bms[base_map_dict[bml.base_map_id]].append(bml.store)
|
||||||
return [
|
return [
|
||||||
BaseMap(name=bm, stores=bmls)
|
BaseMapWithStores(name=bm, stores=bmls)
|
||||||
for bm, bmls in OrderedDict(sorted(bms.items())).items()
|
for bm, bmls in OrderedDict(sorted(bms.items())).items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -84,3 +87,29 @@ async def get_base_style(request: Request, name: str,
|
||||||
else:
|
else:
|
||||||
style = base_style.style # type: ignore
|
style = base_style.style # type: ignore
|
||||||
return BaseStyle(name=name, style=style) # type: ignore
|
return BaseStyle(name=name, style=style) # type: ignore
|
||||||
|
|
||||||
|
@api.get('/layer_style/{store}')
|
||||||
|
async def get_layer_style(request: Request, store: str,
|
||||||
|
response: Response,
|
||||||
|
) -> MaplibreStyle | None:
|
||||||
|
store_record = registry.stores.loc[store]
|
||||||
|
if store_record.is_live:
|
||||||
|
## No ttag for live layers' style (could be added?)
|
||||||
|
## Get layer_defs from live redis and give symbol
|
||||||
|
return await redis_store.get_maplibre_style(store)
|
||||||
|
## Set the etag based on the last modification of the model's style.
|
||||||
|
#if store in info.schema.app['registry'].geom_auto:
|
||||||
|
if store_record.custom:
|
||||||
|
## The style is in Qml
|
||||||
|
ttag_channel = 'gisaf_map.qml'
|
||||||
|
else:
|
||||||
|
## The style is in Category
|
||||||
|
ttag_channel = 'gisaf_survey.category'
|
||||||
|
## Check if the request was etagged:
|
||||||
|
ttag = await redis_store.get_ttag(ttag_channel)
|
||||||
|
if ttag and request.headers.get('If-None-Match') == ttag:
|
||||||
|
raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED)
|
||||||
|
# request.not_modified = True
|
||||||
|
# return MaplibreStyle()
|
||||||
|
response.headers['ETag'] = ttag
|
||||||
|
return await store_record.model.get_maplibre_style()
|
||||||
|
|
|
@ -3,15 +3,17 @@ from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, responses
|
from fastapi import FastAPI, responses
|
||||||
|
|
||||||
from gisaf.api.v2 import api
|
|
||||||
from gisaf.api.geoapi import api as geoapi
|
|
||||||
from gisaf.config import conf
|
from gisaf.config import conf
|
||||||
from gisaf.registry import registry
|
from gisaf.registry import registry
|
||||||
from gisaf.redis_tools import setup_redis, setup_redis_cache, shutdown_redis
|
from gisaf.redis_tools import setup_redis, setup_redis_cache, shutdown_redis
|
||||||
from gisaf.tiles import registry as map_tile_registry
|
from gisaf.tiles import registry as map_tile_registry
|
||||||
from gisaf.live import setup_live
|
from gisaf.live import setup_live
|
||||||
from gisaf.admin import manager as admin_manager
|
from gisaf.admin import manager as admin_manager
|
||||||
|
from gisaf.api.main import api
|
||||||
|
from gisaf.api.geoapi import api as geoapi
|
||||||
from gisaf.api.admin import api as admin_api
|
from gisaf.api.admin import api as admin_api
|
||||||
|
from gisaf.api.dashboard import api as dashboard_api
|
||||||
|
from gisaf.api.map import api as map_api
|
||||||
|
|
||||||
logging.basicConfig(level=conf.gisaf.debugLevel)
|
logging.basicConfig(level=conf.gisaf.debugLevel)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -37,6 +39,8 @@ app = FastAPI(
|
||||||
default_response_class=responses.ORJSONResponse,
|
default_response_class=responses.ORJSONResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.mount('/v2', api)
|
app.include_router(api, prefix="/api")
|
||||||
app.mount('/gj', geoapi)
|
app.include_router(geoapi, prefix="/api/gj")
|
||||||
app.mount('/admin', admin_api)
|
app.include_router(admin_api, prefix="/api/admin")
|
||||||
|
app.include_router(dashboard_api, prefix="/api/dashboard")
|
||||||
|
app.include_router(map_api, prefix='/api/map')
|
|
@ -1,3 +1,4 @@
|
||||||
|
from re import A
|
||||||
import geopandas as gpd
|
import geopandas as gpd
|
||||||
from shapely import from_wkb
|
from shapely import from_wkb
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -5,7 +6,8 @@ from json import dumps
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from gisaf.config import conf
|
from gisaf.config import conf
|
||||||
from gisaf.models.to_migrate import MapboxPaint, MapboxLayout, FeatureInfo
|
from gisaf.models.map_bases import MaplibreStyle
|
||||||
|
from gisaf.models.to_migrate import FeatureInfo
|
||||||
|
|
||||||
|
|
||||||
class BaseStore(SQLModel):
|
class BaseStore(SQLModel):
|
||||||
|
@ -13,8 +15,8 @@ class BaseStore(SQLModel):
|
||||||
name: str = '<Unnamed store>'
|
name: str = '<Unnamed store>'
|
||||||
description: str = '<Description>'
|
description: str = '<Description>'
|
||||||
icon: str | None = None
|
icon: str | None = None
|
||||||
mapbox_paint: MapboxPaint | None = None
|
mapbox_paint: dict[str, dict] | None = None
|
||||||
mapbox_layout: MapboxLayout | None = None
|
mapbox_layout: dict[str, dict] | None = None
|
||||||
attribution: str | None = None
|
attribution: str | None = None
|
||||||
symbol: str = '\ue32b'
|
symbol: str = '\ue32b'
|
||||||
base_gis_type: str = 'Point'
|
base_gis_type: str = 'Point'
|
||||||
|
@ -117,15 +119,12 @@ class BaseStore(SQLModel):
|
||||||
raise NotImplementedError('Subclasses of BaseStore must implement get_item_params()')
|
raise NotImplementedError('Subclasses of BaseStore must implement get_item_params()')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_mapbox_style(cls):
|
async def get_maplibre_style(cls) -> MaplibreStyle:
|
||||||
"""
|
"""
|
||||||
Get the mapbox style (paint, layout, attribution...)
|
Get the mapbox style (paint, layout, attribution...)
|
||||||
"""
|
"""
|
||||||
style = {}
|
return MaplibreStyle(
|
||||||
if cls.mapbox_paint is not None:
|
paint=cls.mapbox_paint,
|
||||||
style['paint'] = dumps(cls.mapbox_paint)
|
layout=cls.mapbox_layout,
|
||||||
if cls.mapbox_layout is not None:
|
attribution=cls.attribution
|
||||||
style['layout'] = dumps(cls.mapbox_layout)
|
)
|
||||||
if cls.attribution is not None:
|
|
||||||
style['attribution'] = cls.attribution
|
|
||||||
return style
|
|
|
@ -27,7 +27,7 @@ class UserBase(SQLModel):
|
||||||
|
|
||||||
class User(UserBase, table=True):
|
class User(UserBase, table=True):
|
||||||
__table_args__ = gisaf_admin.table_args
|
__table_args__ = gisaf_admin.table_args
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: str | 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)
|
||||||
password: str | None = None
|
password: str | None = None
|
||||||
|
|
|
@ -61,8 +61,8 @@ class CategoryBase(BaseModel):
|
||||||
style: str | None = Field(sa_type=TEXT)
|
style: str | None = Field(sa_type=TEXT)
|
||||||
symbol: str | None = Field(sa_type=String(1)) # type: ignore
|
symbol: str | None = Field(sa_type=String(1)) # type: ignore
|
||||||
mapbox_type_custom: str | None = Field(sa_type=String(12)) # type: ignore
|
mapbox_type_custom: str | None = Field(sa_type=String(12)) # type: ignore
|
||||||
mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
mapbox_paint: dict[str, dict | list | float | int | str] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
mapbox_layout: dict[str, dict | list | float | int | str] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
viewable_role: str | None
|
viewable_role: str | None
|
||||||
extra: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
extra: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
|
|
||||||
|
|
|
@ -14,20 +14,20 @@ import shapely # type: ignore
|
||||||
import pyproj
|
import pyproj
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlmodel import select, Field, Relationship
|
from sqlmodel import select, Field
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from sqlalchemy import BigInteger, MetaData, String, func, and_, text
|
from sqlalchemy import BigInteger, String, func, and_, text
|
||||||
from sqlalchemy.sql import sqltypes
|
from sqlalchemy.sql import sqltypes
|
||||||
from sqlalchemy.orm import declared_attr
|
from sqlalchemy.orm import declared_attr
|
||||||
from psycopg2.extensions import adapt
|
from psycopg2.extensions import adapt
|
||||||
from geoalchemy2.shape import from_shape
|
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_wkb
|
||||||
from shapely.geometry import mapping
|
from shapely.geometry import mapping # type: ignore
|
||||||
from shapely.ops import transform # type: ignore
|
from shapely.ops import transform # type: ignore
|
||||||
|
|
||||||
from shapefile import (
|
from shapefile import ( # type: ignore
|
||||||
Writer as ShapeFileWriter, # type: ignore
|
Writer as ShapeFileWriter,
|
||||||
POINT, POINTZ,
|
POINT, POINTZ,
|
||||||
POLYLINE, POLYLINEZ,
|
POLYLINE, POLYLINEZ,
|
||||||
POLYGON, POLYGONZ,
|
POLYGON, POLYGONZ,
|
||||||
|
@ -35,8 +35,9 @@ from shapefile import (
|
||||||
|
|
||||||
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.map_bases import MaplibreStyle
|
||||||
from gisaf.models.models_base import Model
|
from gisaf.models.models_base import Model
|
||||||
from gisaf.models.metadata import gisaf_survey, gisaf_admin, survey
|
from gisaf.models.metadata import gisaf_survey, gisaf_admin, survey, raw_survey
|
||||||
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.to_migrate import InfoItem
|
from gisaf.models.to_migrate import InfoItem
|
||||||
|
@ -157,6 +158,7 @@ class SurveyModel(BaseSurveyModel):
|
||||||
# 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
|
||||||
|
category_name: ClassVar[str]
|
||||||
|
|
||||||
filtered_columns_on_map: ClassVar[list[str]] = [
|
filtered_columns_on_map: ClassVar[list[str]] = [
|
||||||
'equip_id',
|
'equip_id',
|
||||||
|
@ -219,9 +221,8 @@ class SurveyModel(BaseSurveyModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_geojson(cls,
|
async def get_geojson(cls,
|
||||||
registry=None, simplify_tolerance=0, preserve_topology=False):
|
simplify_tolerance=0, preserve_topology=False):
|
||||||
if registry is None:
|
from gisaf.registry import registry
|
||||||
from ..registry import registry
|
|
||||||
|
|
||||||
## Fastest, but the id is in properties and needs the front end (eg Gisaf for mapbox)
|
## Fastest, but the id is in properties and needs the front end (eg Gisaf for mapbox)
|
||||||
## to move it at the feature level
|
## to move it at the feature level
|
||||||
|
@ -592,36 +593,35 @@ class GeoModelNoStatus(Model):
|
||||||
return zip_file
|
return zip_file
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_mapbox_style(cls):
|
async def get_maplibre_style(cls) -> MaplibreStyle | None:
|
||||||
"""
|
"""
|
||||||
Get the mapbox style (paint, layout, attribution...)
|
Get the mapbox style (paint, layout, attribution...)
|
||||||
"""
|
"""
|
||||||
## If the model is from survey, it should have a category, which has a style
|
## If the model is from survey, it should have a category, which has a style
|
||||||
## Get from database
|
## Get from database
|
||||||
style = {}
|
style: MaplibreStyle | None
|
||||||
if hasattr(cls, 'category'):
|
async with db_session() as session:
|
||||||
category = await Category.get_item_by_pk(pk=cls.category.name)
|
if hasattr(cls, 'category'):
|
||||||
if category:
|
category = await session.get(Category, cls.get_store_name())
|
||||||
if category['mapbox_paint'] is not None:
|
if category:
|
||||||
style['paint'] = category['mapbox_paint']
|
style = MaplibreStyle(
|
||||||
if category['mapbox_layout'] is not None:
|
layout=category.mapbox_layout,
|
||||||
style['layout'] = category['mapbox_layout']
|
paint=category.mapbox_paint,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
category = None
|
style = None
|
||||||
|
else:
|
||||||
qml = await Qml.get_item_by_pk(pk=cls.__name__)
|
qml = await session.get(Qml, cls.__name__)
|
||||||
if qml:
|
if qml:
|
||||||
if qml['mapbox_paint']:
|
style = MaplibreStyle(
|
||||||
style['paint'] = qml['mapbox_paint']
|
layout=qml.mapbox_layout,
|
||||||
if qml['mapbox_layout']:
|
paint=qml.mapbox_paint,
|
||||||
style['layout'] = qml['mapbox_layout']
|
attribution=qml.attr,
|
||||||
|
)
|
||||||
if cls.attribution is not None:
|
else:
|
||||||
style['attribution'] = cls.attribution
|
style = None
|
||||||
return style
|
return style
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_features_attrs(cls, simplify_tolerance):
|
async def get_features_attrs(cls, simplify_tolerance):
|
||||||
"""
|
"""
|
||||||
|
@ -1094,10 +1094,13 @@ 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
|
__table_args__ = raw_survey.table_args
|
||||||
geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3,
|
geom: Annotated[str, WKBElement] = Field(
|
||||||
srid=conf.geo.raw_survey.srid))
|
sa_type=Geometry('POINTZ',
|
||||||
status: str = Field(sa_type=String(1))
|
dimension=3,
|
||||||
|
srid=conf.geo.raw_survey.srid),
|
||||||
|
) # type: ignore
|
||||||
|
status: str = Field(sa_type=String(1)) # type: ignore
|
||||||
|
|
||||||
## store_name is set in category_models_maker.make_category_models
|
## store_name is set in category_models_maker.make_category_models
|
||||||
store_name: ClassVar[str | None] = None
|
store_name: ClassVar[str | None] = None
|
||||||
|
|
|
@ -28,30 +28,6 @@ class BaseStyle(Model, table=True):
|
||||||
return f'<models.BaseStyle {self.name:s}>'
|
return f'<models.BaseStyle {self.name:s}>'
|
||||||
|
|
||||||
|
|
||||||
class BaseMap(Model, table=True):
|
|
||||||
__table_args__ = gisaf_map.table_args
|
|
||||||
__tablename__: str = 'base_map' # type: ignore
|
|
||||||
|
|
||||||
class Admin:
|
|
||||||
menu = 'Other'
|
|
||||||
|
|
||||||
id: int | None = Field(primary_key=True, default=None)
|
|
||||||
name: str
|
|
||||||
layers: list['BaseMapLayer'] = Relationship(back_populates='base_map')
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'<models.BaseMap {self.name:s}>'
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def selectinload(cls) -> list[list['BaseMapLayer']]:
|
|
||||||
return [
|
|
||||||
cls.layers
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMapLayer(Model, table=True):
|
class BaseMapLayer(Model, table=True):
|
||||||
__table_args__ = gisaf_map.table_args
|
__table_args__ = gisaf_map.table_args
|
||||||
__tablename__: str = 'base_map_layer' # type: ignore
|
__tablename__: str = 'base_map_layer' # type: ignore
|
||||||
|
@ -62,7 +38,7 @@ class BaseMapLayer(Model, table=True):
|
||||||
id: int | None = Field(primary_key=True, default=None)
|
id: int | None = Field(primary_key=True, default=None)
|
||||||
base_map_id: int = Field(foreign_key=gisaf_map.table('base_map.id'),
|
base_map_id: int = Field(foreign_key=gisaf_map.table('base_map.id'),
|
||||||
index=True)
|
index=True)
|
||||||
base_map: BaseMap = Relationship(back_populates='layers')
|
base_map: 'BaseMap' = Relationship() #back_populates='layers')
|
||||||
store: str = Field(sa_type=String(100)) # type: ignore
|
store: str = Field(sa_type=String(100)) # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -78,8 +54,46 @@ class BaseMapLayer(Model, table=True):
|
||||||
return f"{self.store or '':s}"
|
return f"{self.store or '':s}"
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMap(Model, table=True):
|
||||||
|
__table_args__ = gisaf_map.table_args
|
||||||
|
__tablename__: str = 'base_map' # type: ignore
|
||||||
|
|
||||||
|
class Admin:
|
||||||
|
menu = 'Other'
|
||||||
|
|
||||||
|
id: int | None = Field(primary_key=True, default=None)
|
||||||
|
name: str
|
||||||
|
# layers: list['BaseMapLayer'] = Relationship(
|
||||||
|
# back_populates='base_map',
|
||||||
|
# # link_model=BaseMapLayer
|
||||||
|
# )
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<models.BaseMap {self.name:s}>'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def selectinload(cls) -> list[list[BaseMapLayer]]:
|
||||||
|
# return [
|
||||||
|
# cls.layers
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMapWithStores(BaseModel):
|
||||||
|
name: str
|
||||||
|
stores: list[str]
|
||||||
|
|
||||||
|
|
||||||
class MapInitData(BaseModel):
|
class MapInitData(BaseModel):
|
||||||
baseStyles: list[BaseStyle] = []
|
baseStyles: list[BaseStyle] = []
|
||||||
baseMaps: list[BaseMap] = []
|
baseMaps: list[BaseMapWithStores] = []
|
||||||
groups: list[CategoryGroup] = []
|
groups: list[CategoryGroup] = []
|
||||||
stores: list[Store] = []
|
stores: list[Store] = []
|
||||||
|
|
||||||
|
|
||||||
|
class MaplibreStyle(BaseModel):
|
||||||
|
paint: dict[str, dict | list | float | int | str] | None = None
|
||||||
|
layout: dict[str, dict | list | float | int | str] | None = None
|
||||||
|
attribution: str | None = None
|
|
@ -14,11 +14,11 @@ class NotADataframeError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Qml(Model):
|
class Qml(Model, table=True):
|
||||||
"""
|
"""
|
||||||
Model for storing qml (QGis style)
|
Model for storing qml (QGis style)
|
||||||
"""
|
"""
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=()) # type: ignore
|
||||||
__table_args__ = gisaf_map.table_args
|
__table_args__ = gisaf_map.table_args
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
|
@ -26,11 +26,11 @@ class Qml(Model):
|
||||||
flask_admin_model_view = 'QmlModelView'
|
flask_admin_model_view = 'QmlModelView'
|
||||||
|
|
||||||
model_name: str | None = Field(default=None, primary_key=True)
|
model_name: str | None = Field(default=None, primary_key=True)
|
||||||
qml: str
|
qml: str | None = None
|
||||||
attr: str
|
attr: str | None = None
|
||||||
style: str
|
style: str | None = None
|
||||||
mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True))
|
mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True))
|
mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<models.Qml {self.model_name:s}>'.format(self=self)
|
return '<models.Qml {self.model_name:s}>'.format(self=self)
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
from typing import Any
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from gisaf.models.geo_models_base import GeoModel, RawSurveyBaseModel, GeoPointSurveyModel
|
|
||||||
|
|
||||||
|
|
||||||
class MapLibreStyle(BaseModel):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class StoreNameOnly(BaseModel):
|
class StoreNameOnly(BaseModel):
|
||||||
|
@ -12,6 +6,7 @@ class StoreNameOnly(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Store(StoreNameOnly):
|
class Store(StoreNameOnly):
|
||||||
|
category: str | None = None
|
||||||
auto_import: bool
|
auto_import: bool
|
||||||
# base_gis_type: str
|
# base_gis_type: str
|
||||||
count: int | None = None
|
count: int | None = None
|
||||||
|
@ -39,7 +34,6 @@ class Store(StoreNameOnly):
|
||||||
#raw_model: GeoPointSurveyModel
|
#raw_model: GeoPointSurveyModel
|
||||||
#raw_model_store_name: str
|
#raw_model_store_name: str
|
||||||
status: str
|
status: str
|
||||||
store: str
|
|
||||||
style: str | None
|
style: str | None
|
||||||
symbol: str | None
|
symbol: str | None
|
||||||
title: str
|
title: str
|
||||||
|
|
|
@ -73,11 +73,3 @@ class FeatureInfo(BaseModel):
|
||||||
files: list[Attachment] = []
|
files: list[Attachment] = []
|
||||||
images: list[Attachment] = []
|
images: list[Attachment] = []
|
||||||
externalRecordUrl: str | None = None
|
externalRecordUrl: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class MapboxPaint(BaseModel):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class MapboxLayout(BaseModel):
|
|
||||||
...
|
|
|
@ -16,6 +16,7 @@ from redis import asyncio as aioredis
|
||||||
|
|
||||||
from gisaf.config import conf
|
from gisaf.config import conf
|
||||||
# from gisaf.models.live import LiveModel
|
# from gisaf.models.live import LiveModel
|
||||||
|
from gisaf.models.map_bases import MaplibreStyle
|
||||||
from gisaf.utils import (SHAPELY_TYPE_TO_MAPBOX_TYPE, DEFAULT_MAPBOX_LAYOUT,
|
from gisaf.utils import (SHAPELY_TYPE_TO_MAPBOX_TYPE, DEFAULT_MAPBOX_LAYOUT,
|
||||||
DEFAULT_MAPBOX_PAINT, gisTypeSymbolMap)
|
DEFAULT_MAPBOX_PAINT, gisTypeSymbolMap)
|
||||||
from gisaf.registry import registry
|
from gisaf.registry import registry
|
||||||
|
@ -281,18 +282,14 @@ class Store:
|
||||||
registry.geom_live_defs[model_info['store']] = model_info
|
registry.geom_live_defs[model_info['store']] = model_info
|
||||||
registry.update_live_layers()
|
registry.update_live_layers()
|
||||||
|
|
||||||
async def get_mapbox_style(self, store_name):
|
async def get_maplibre_style(self, store_name) -> MaplibreStyle:
|
||||||
"""
|
"""
|
||||||
Get the http headers (mapbox style) from the store name (layer_def)
|
Get the http headers (mapbox style) from the store name (layer_def)
|
||||||
"""
|
"""
|
||||||
paint = await self.redis.get(self.get_mapbox_paint_channel(store_name))
|
return MaplibreStyle(
|
||||||
layout = await self.redis.get(self.get_mapbox_layout_channel(store_name))
|
paint=(await self.redis.get(self.get_mapbox_paint_channel(store_name))).decode(),
|
||||||
style = {}
|
layout=(await self.redis.get(self.get_mapbox_layout_channel(store_name))).decode(),
|
||||||
if paint is not None:
|
)
|
||||||
style['paint'] = paint.decode()
|
|
||||||
if layout is not None:
|
|
||||||
style['layout'] = layout.decode()
|
|
||||||
return style
|
|
||||||
|
|
||||||
async def get_layer_as_json(self, store_name):
|
async def get_layer_as_json(self, store_name):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,7 +12,7 @@ from pydantic import create_model
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import selectinload, joinedload
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from sqlmodel import SQLModel, select, inspect, Relationship
|
from sqlmodel import SQLModel, col, select, inspect, Relationship
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ class ModelRegistry:
|
||||||
# for category in categories
|
# for category in categories
|
||||||
# if self.raw_survey_models.get(category.table_name)}
|
# if self.raw_survey_models.get(category.table_name)}
|
||||||
|
|
||||||
async def get_model_id_params(self, model: SQLModel, id: int) -> FeatureInfo:
|
async def get_model_id_params(self, model: SQLModel, id: int) -> FeatureInfo | None:
|
||||||
"""
|
"""
|
||||||
Return the parameters for this item (table name, id), displayed in info pane
|
Return the parameters for this item (table name, id), displayed in info pane
|
||||||
"""
|
"""
|
||||||
|
@ -389,8 +389,9 @@ class ModelRegistry:
|
||||||
"""
|
"""
|
||||||
## Utility functions used with apply method (dataframes)
|
## Utility functions used with apply method (dataframes)
|
||||||
def fill_columns_from_custom_models(row) -> tuple[str, str, str, str, str, str, str]:
|
def fill_columns_from_custom_models(row) -> tuple[str, str, str, str, str, str, str]:
|
||||||
|
# model:
|
||||||
return (
|
return (
|
||||||
row.model.__name__,
|
row.model.get_store_name(),
|
||||||
row.model.__name__,
|
row.model.__name__,
|
||||||
row.model.description,
|
row.model.description,
|
||||||
row.model.__table__.schema,
|
row.model.__table__.schema,
|
||||||
|
@ -429,7 +430,11 @@ class ModelRegistry:
|
||||||
self.categories = self.categories.merge(df_raw_models, left_on='store', right_index=True)
|
self.categories = self.categories.merge(df_raw_models, left_on='store', right_index=True)
|
||||||
self.categories['custom'] = False
|
self.categories['custom'] = False
|
||||||
self.categories['is_db'] = True
|
self.categories['is_db'] = True
|
||||||
self.categories.sort_index(inplace=True)
|
self.categories.reset_index(inplace=True)
|
||||||
|
self.categories.rename(columns={'name': 'category'}, inplace=True)
|
||||||
|
self.categories.set_index('store', inplace=True)
|
||||||
|
self.categories.sort_values('category')
|
||||||
|
# self.categories.sort_index(inplace=True)
|
||||||
# self.categories['name_letter'] = self.categories.index.str.slice(0, 1)
|
# self.categories['name_letter'] = self.categories.index.str.slice(0, 1)
|
||||||
# self.categories['name_number'] = self.categories.index.str.slice(1).astype('int64')
|
# self.categories['name_number'] = self.categories.index.str.slice(1).astype('int64')
|
||||||
# self.categories.sort_values(['name_letter', 'name_number'], inplace=True)
|
# self.categories.sort_values(['name_letter', 'name_number'], inplace=True)
|
||||||
|
@ -467,6 +472,7 @@ class ModelRegistry:
|
||||||
self.custom_models['custom'] = True
|
self.custom_models['custom'] = True
|
||||||
self.custom_models['is_db'] = True
|
self.custom_models['is_db'] = True
|
||||||
self.custom_models['raw_model_store_name'] = ''
|
self.custom_models['raw_model_store_name'] = ''
|
||||||
|
self.custom_models['category'] = ''
|
||||||
self.custom_models['in_menu'] = self.custom_models.apply(
|
self.custom_models['in_menu'] = self.custom_models.apply(
|
||||||
lambda row: getattr(row.model, 'in_menu', True),
|
lambda row: getattr(row.model, 'in_menu', True),
|
||||||
axis=1
|
axis=1
|
||||||
|
@ -512,6 +518,7 @@ class ModelRegistry:
|
||||||
self.custom_stores = self.custom_stores.loc[self.custom_stores.in_menu]
|
self.custom_stores = self.custom_stores.loc[self.custom_stores.in_menu]
|
||||||
self.custom_stores['auto_import'] = False
|
self.custom_stores['auto_import'] = False
|
||||||
self.custom_stores['is_line_work'] = False
|
self.custom_stores['is_line_work'] = False
|
||||||
|
self.custom_stores['category'] = ''
|
||||||
|
|
||||||
if len(self.custom_stores) > 0:
|
if len(self.custom_stores) > 0:
|
||||||
self.custom_stores['long_name'],\
|
self.custom_stores['long_name'],\
|
||||||
|
@ -525,13 +532,18 @@ class ModelRegistry:
|
||||||
## Combine Misc (custom) and survey (auto) stores
|
## Combine Misc (custom) and survey (auto) stores
|
||||||
## Retain only one status per category (defaultStatus, 'E'/existing by default)
|
## Retain only one status per category (defaultStatus, 'E'/existing by default)
|
||||||
self.stores = pd.concat([
|
self.stores = pd.concat([
|
||||||
self.categories[self.categories.status==conf.map.defaultStatus[0]].reset_index().set_index('store').sort_values('title'),
|
self.categories[self.categories.status==conf.map.defaultStatus[0]].sort_values('title'),
|
||||||
self.custom_models,
|
self.custom_models,
|
||||||
self.custom_stores
|
self.custom_stores
|
||||||
])#.drop(columns=['store_name'])
|
])#.drop(columns=['store_name'])
|
||||||
|
self.stores.drop(columns='name', inplace=True)
|
||||||
|
self.stores.index.name = 'name'
|
||||||
self.stores['in_menu'] = self.stores['in_menu'].astype(bool)
|
self.stores['in_menu'] = self.stores['in_menu'].astype(bool)
|
||||||
self.stores['status'].fillna('E', inplace=True)
|
self.stores['status'].fillna('E', inplace=True)
|
||||||
|
|
||||||
|
self.categories.reset_index(inplace=True)
|
||||||
|
self.categories.set_index('category', inplace=True)
|
||||||
|
|
||||||
## Set in the stores dataframe some useful properties, from the model class
|
## Set in the stores dataframe some useful properties, from the model class
|
||||||
## Maybe at some point it makes sense to get away from class-based definitions
|
## Maybe at some point it makes sense to get away from class-based definitions
|
||||||
def fill_columns_from_model(row):
|
def fill_columns_from_model(row):
|
||||||
|
@ -576,18 +588,20 @@ class ModelRegistry:
|
||||||
## Add Misc and Live
|
## Add Misc and Live
|
||||||
self.primary_groups.append(CategoryGroup(
|
self.primary_groups.append(CategoryGroup(
|
||||||
name='Misc',
|
name='Misc',
|
||||||
|
long_name='Misc',
|
||||||
major=True,
|
major=True,
|
||||||
long_name='Misc and old layers (not coming from our survey; '
|
# description]='Misc and old layers (not coming from our survey; '
|
||||||
'they will be organized, '
|
# 'they will be organized, '
|
||||||
'eventually as the surveys get more complete)',
|
# 'eventually as the surveys get more complete)',
|
||||||
categories=[],
|
categories=[],
|
||||||
))
|
))
|
||||||
|
|
||||||
self.primary_groups.append(CategoryGroup(
|
self.primary_groups.append(CategoryGroup(
|
||||||
name='Live',
|
name='Live',
|
||||||
|
long_name='Live',
|
||||||
major=True,
|
major=True,
|
||||||
long_name='Layers from data processing, sensors, etc, '
|
# long_name='Layers from data processing, sensors, etc, '
|
||||||
'and are updated automatically',
|
# 'and are updated automatically',
|
||||||
categories=[],
|
categories=[],
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -671,7 +685,7 @@ class ModelRegistry:
|
||||||
df_live['auto_import'] = False
|
df_live['auto_import'] = False
|
||||||
df_live['base_gis_type'] = df_live['gis_type']
|
df_live['base_gis_type'] = df_live['gis_type']
|
||||||
df_live['custom'] = False
|
df_live['custom'] = False
|
||||||
df_live['group'] = ''
|
df_live['group'] = 'Live'
|
||||||
df_live['in_menu'] = True
|
df_live['in_menu'] = True
|
||||||
df_live['is_db'] = False
|
df_live['is_db'] = False
|
||||||
df_live['is_line_work'] = False
|
df_live['is_line_work'] = False
|
||||||
|
@ -681,8 +695,11 @@ class ModelRegistry:
|
||||||
df_live['minor_group_2'] = ''
|
df_live['minor_group_2'] = ''
|
||||||
df_live['status'] = 'E'
|
df_live['status'] = 'E'
|
||||||
df_live['style'] = None
|
df_live['style'] = None
|
||||||
df_live['title'] = df_live['name']
|
df_live['category'] = ''
|
||||||
|
df_live.rename(columns={'name': 'title'}, inplace=True)
|
||||||
|
df_live.index.name = 'name'
|
||||||
registry.stores = pd.concat([registry.stores, df_live])
|
registry.stores = pd.concat([registry.stores, df_live])
|
||||||
|
df_live.index.name = 'store'
|
||||||
for store, model_info in self.geom_live_defs.items():
|
for store, model_info in self.geom_live_defs.items():
|
||||||
## Add provided live layers in the stores df
|
## Add provided live layers in the stores df
|
||||||
# Create the pydantic model
|
# Create the pydantic model
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue