55# Angelos Tzotsos <tzotsos@gmail.com>
66# Ricardo Garcia Silva <ricardo.garcia.silva@gmail.com>
77#
8- # Copyright (c) 2024 Tom Kralidis
8+ # Copyright (c) 2025 Tom Kralidis
99# Copyright (c) 2015 Angelos Tzotsos
1010# Copyright (c) 2017 Ricardo Garcia Silva
1111#
3434
3535import inspect
3636import logging
37+ from operator import itemgetter
3738import os
3839from time import sleep
3940
4950from pycsw .core import util
5051from pycsw .core .etree import etree
5152from pycsw .core .etree import PARSER
53+ from pycsw .core .pygeofilter_ext import to_filter
5254
5355LOGGER = logging .getLogger (__name__ )
5456
5557
56- class Repository ( object ) :
58+ class Repository :
5759 _engines = {}
5860
5961 @classmethod
@@ -87,14 +89,15 @@ def connect(dbapi_connection, connection_rec):
8789 return clazz ._engines [url ]
8890
8991 ''' Class to interact with underlying repository '''
90- def __init__ (self , database , context , app_root = None , table = 'records' , repo_filter = None ):
92+ def __init__ (self , repo_object , context , app_root = None ):
9193 ''' Initialize repository '''
9294
9395 self .context = context
94- self .filter = repo_filter
96+ self .filter = repo_object . get ( 'filter' )
9597 self .fts = False
96- self .database = database
97- self .table = table
98+ self .database = repo_object .get ('database' )
99+ self .table = repo_object .get ('table' )
100+ self .facets = repo_object .get ('facets' , [])
98101
99102 # Don't use relative paths, this is hack to get around
100103 # most wsgi restriction...
@@ -110,7 +113,7 @@ def __init__(self, database, context, app_root=None, table='records', repo_filte
110113
111114 self .postgis_geometry_column = None
112115
113- schema_name , table_name = table .rpartition ("." )[::2 ]
116+ schema_name , table_name = self . table .rpartition ("." )[::2 ]
114117
115118 default_table_args = {
116119 "autoload" : True ,
@@ -145,6 +148,7 @@ def __init__(self, database, context, app_root=None, table='records', repo_filte
145148 temp_dbtype = None
146149
147150 self .query_mappings = {
151+ # OGC API - Records mappings
148152 'identifier' : self .dataset .identifier ,
149153 'type' : self .dataset .type ,
150154 'typename' : self .dataset .typename ,
@@ -171,6 +175,10 @@ def __init__(self, database, context, app_root=None, table='records', repo_filte
171175 'gsd' : self .dataset .distancevalue
172176 }
173177
178+ LOGGER .debug ('adding OGC CSW mappings' )
179+ for key , value in self .context .models ['csw' ]['typenames' ]['csw:Record' ]['queryables' ]['SupportedDublinCoreQueryables' ].items ():
180+ self .query_mappings [key ] = util .getqattr (self .dataset , value ['dbcol' ])
181+
174182 if self .dbtype == 'postgresql' :
175183 # check if PostgreSQL is enabled with PostGIS 1.x
176184 try :
@@ -415,18 +423,34 @@ def query_source(self, source):
415423 query = self .session .query (self .dataset ).filter (column == source )
416424 return self ._get_repo_filter (query ).all ()
417425
418- def query (self , constraint , sortby = None , typenames = None ,
426+ def query (self , constraint = None , sortby = None , typenames = None ,
419427 maxrecords = 10 , startposition = 0 ):
420428 ''' Query records from underlying repository '''
421429
422- # run the raw query and get total
423- if 'where' in constraint : # GetRecords with constraint
424- LOGGER .debug ('constraint detected' )
425- query = self .session .query (self .dataset ).filter (
426- text (constraint ['where' ])).params (self ._create_values (constraint ['values' ]))
427- else : # GetRecords sans constraint
428- LOGGER .debug ('No constraint detected' )
429- query = self .session .query (self .dataset )
430+ if constraint .get ('ast' ) is not None : # GetRecords with pygeofilter AST
431+ LOGGER .debug ('pygeofilter AST detected' )
432+ LOGGER .debug ('Transforming AST into filters' )
433+ try :
434+ filters = to_filter (constraint ['ast' ], self .dbtype , self .query_mappings )
435+ LOGGER .debug (f'Filter: { filters } ' )
436+ except Exception as err :
437+ msg = f'AST evaluator error: { str (err )} '
438+ LOGGER .exception (msg )
439+ raise RuntimeError (msg )
440+
441+ query = self .session .query (self .dataset ).filter (filters )
442+
443+ else : # GetRecords sans pygeofilter AST
444+ LOGGER .debug ('No pygeofilter AST detected' )
445+
446+ # run the raw query and get total
447+ if 'where' in constraint : # GetRecords with constraint
448+ LOGGER .debug ('constraint detected' )
449+ query = self .session .query (self .dataset ).filter (
450+ text (constraint ['where' ])).params (self ._create_values (constraint ['values' ]))
451+ else : # GetRecords sans constraint
452+ LOGGER .debug ('No constraint detected' )
453+ query = self .session .query (self .dataset )
430454
431455 total = self ._get_repo_filter (query ).count ()
432456
@@ -443,7 +467,10 @@ def query(self, constraint, sortby=None, typenames=None,
443467 if sortby is not None : # apply sorting
444468 LOGGER .debug ('sorting detected' )
445469 # TODO: Check here for dbtype so to extract wkt from postgis native to wkt
446- sortby_column = getattr (self .dataset , sortby ['propertyname' ])
470+ try :
471+ sortby_column = getattr (self .dataset , sortby ['propertyname' ])
472+ except :
473+ sortby_column = self .query_mappings .get (sortby ['propertyname' ])
447474
448475 if sortby ['order' ] == 'DESC' : # descending sort
449476 if 'spatial' in sortby and sortby ['spatial' ]: # spatial sort
@@ -457,9 +484,50 @@ def query(self, constraint, sortby=None, typenames=None,
457484 query = query .order_by (sortby_column )
458485
459486 # always apply limit and offset
460- return [str ( total ) , self ._get_repo_filter (query ).limit (
487+ return [total , self ._get_repo_filter (query ).limit (
461488 maxrecords ).offset (startposition ).all ()]
462489
490+ def get_facets (self , ast = None ) -> dict :
491+ """
492+ Gets all facets for a given query
493+
494+ :returns: `dict` of facets
495+ """
496+
497+ facets_results = {}
498+
499+ for facet in self .facets :
500+ LOGGER .debug (f'Running facet for { facet } ' )
501+ facetq = self .session .query (self .query_mappings [facet ], self .func .count (facet )).group_by (facet )
502+
503+ if ast is not None :
504+ try :
505+ filters = to_filter (ast , self .dbtype , self .query_mappings )
506+ LOGGER .debug (f'Filter: { filters } ' )
507+ except Exception as err :
508+ msg = f'AST evaluator error: { str (err )} '
509+ LOGGER .exception (msg )
510+ raise RuntimeError (msg )
511+
512+ facetq = facetq .filter (filters )
513+
514+ LOGGER .debug ('Writing facet query results' )
515+ facets_results [facet ] = {
516+ 'type' : 'terms' ,
517+ 'property' : facet ,
518+ 'buckets' : []
519+ }
520+
521+ for fq in facetq .all ():
522+ facets_results [facet ]['buckets' ].append ({
523+ 'value' : fq [0 ],
524+ 'count' : fq [1 ]
525+ })
526+
527+ facets_results [facet ]['buckets' ].sort (key = itemgetter ('count' ), reverse = True )
528+
529+ return facets_results
530+
463531 def insert (self , record , source , insert_date ):
464532 ''' Insert a record into the repository '''
465533
0 commit comments