import asyncio import json from pathlib import Path import logging import pandas as pd from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.engine.row import Row from sqlalchemy.sql.selectable import Select import geopandas as gpd # type: ignore from treetrail.config import conf logger = logging.getLogger(__name__) class AlchemyEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj.__class__, DeclarativeMeta): # an SQLAlchemy class fields = {} for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']: data = obj.__getattribute__(field) try: # this will fail on non-encodable values, like other classes json.dumps(data) fields[field] = data except TypeError: fields[field] = None # a json-encodable dict return fields if isinstance(obj, Row): return dict(obj) return json.JSONEncoder.default(self, obj) async def read_sql_async(stmt, con): loop = asyncio.get_event_loop() return await loop.run_in_executor(None, pd.read_sql, stmt, con) def read_sql(con, stmt): ## See https://stackoverflow.com/questions/70848256/how-can-i-use-pandas-read-sql-on-an-async-connection return pd.read_sql_query(stmt, con) def get_attachment_root(type: str): return Path(conf.storage.root_attachment_path) / type def get_attachment_tree_root(): return get_attachment_root('tree') def get_attachment_trail_root(): return get_attachment_root('trail') def get_attachment_poi_root(): return get_attachment_root('poi') def pandas_query(session, query): return pd.read_sql_query(query, session.connection()) def geopandas_query(session, query: Select, model, *, # simplify_tolerance: float|None=None, crs=None, cast=True, ): ## XXX: I could not get the add_columns work without creating a subquery, ## so moving the simplification to geopandas - see in _get_df # if simplify_tolerance is not None: # query = query.with_only_columns(*(col for col in query.columns # if col.name != 'geom')) # new_column = model.__table__.columns['geom'].ST_SimplifyPreserveTopology( # simplify_tolerance).label('geom') # query = query.add_columns(new_column) return gpd.GeoDataFrame.from_postgis(query, session.connection(), crs=crs) def mkdir(dir: Path | str) -> Path: path = Path(dir) if not path.is_dir(): logger.info(f'Create directory {path}') path.mkdir(parents=True, exist_ok=True) return path def get_version() -> str: version_file_src = Path(__file__).parent / 'version.txt' version_file_in_container = Path("/app") / 'version.txt' if version_file_src.exists(): with open(version_file_src) as version: return version.read().strip() if version_file_in_container.exists(): with open(version_file_in_container) as version: return version.read().strip() else: logger.debug('No version file, using git') try: from subprocess import run git_version_cmd = run(['git', 'describe', '--broken', '--tags', '--always', '--dirty'], capture_output=True) if git_version_cmd.returncode == 0: return git_version_cmd.stdout.strip().decode() else: logger.debug('git returns with the error below, version set as 0.0.0') logger.debug(git_version_cmd.stderr.decode()) return '0.0.0' except FileNotFoundError as err: logger.debug('git not found: version set as 0.0.0') return '0.0.0'