55# Angelos Tzotsos <[email protected] > 66# Ricardo Garcia Silva <[email protected] > 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 ,
@@ -167,6 +171,10 @@ def __init__(self, database, context, app_root=None, table='records', repo_filte
167171 'off_nadir' : self .dataset .illuminationelevationangle
168172 }
169173
174+ LOGGER .debug ('adding OGC CSW mappings' )
175+ for key , value in self .context .models ['csw' ]['typenames' ]['csw:Record' ]['queryables' ]['SupportedDublinCoreQueryables' ].items ():
176+ self .query_mappings [key ] = util .getqattr (self .dataset , value ['dbcol' ])
177+
170178 if self .dbtype == 'postgresql' :
171179 # check if PostgreSQL is enabled with PostGIS 1.x
172180 try :
@@ -410,18 +418,34 @@ def query_source(self, source):
410418 query = self .session .query (self .dataset ).filter (column == source )
411419 return self ._get_repo_filter (query ).all ()
412420
413- def query (self , constraint , sortby = None , typenames = None ,
421+ def query (self , constraint = None , sortby = None , typenames = None ,
414422 maxrecords = 10 , startposition = 0 ):
415423 ''' Query records from underlying repository '''
416424
417- # run the raw query and get total
418- if 'where' in constraint : # GetRecords with constraint
419- LOGGER .debug ('constraint detected' )
420- query = self .session .query (self .dataset ).filter (
421- text (constraint ['where' ])).params (self ._create_values (constraint ['values' ]))
422- else : # GetRecords sans constraint
423- LOGGER .debug ('No constraint detected' )
424- query = self .session .query (self .dataset )
425+ if constraint .get ('ast' ) is not None : # GetRecords with pygeofilter AST
426+ LOGGER .debug ('pygeofilter AST detected' )
427+ LOGGER .debug ('Transforming AST into filters' )
428+ try :
429+ filters = to_filter (constraint ['ast' ], self .dbtype , self .query_mappings )
430+ LOGGER .debug (f'Filter: { filters } ' )
431+ except Exception as err :
432+ msg = f'AST evaluator error: { str (err )} '
433+ LOGGER .exception (msg )
434+ raise RuntimeError (msg )
435+
436+ query = self .session .query (self .dataset ).filter (filters )
437+
438+ else : # GetRecords sans pygeofilter AST
439+ LOGGER .debug ('No pygeofilter AST detected' )
440+
441+ # run the raw query and get total
442+ if 'where' in constraint : # GetRecords with constraint
443+ LOGGER .debug ('constraint detected' )
444+ query = self .session .query (self .dataset ).filter (
445+ text (constraint ['where' ])).params (self ._create_values (constraint ['values' ]))
446+ else : # GetRecords sans constraint
447+ LOGGER .debug ('No constraint detected' )
448+ query = self .session .query (self .dataset )
425449
426450 total = self ._get_repo_filter (query ).count ()
427451
@@ -452,9 +476,50 @@ def query(self, constraint, sortby=None, typenames=None,
452476 query = query .order_by (sortby_column )
453477
454478 # always apply limit and offset
455- return [str ( total ) , self ._get_repo_filter (query ).limit (
479+ return [total , self ._get_repo_filter (query ).limit (
456480 maxrecords ).offset (startposition ).all ()]
457481
482+ def get_facets (self , ast = None ) -> dict :
483+ """
484+ Gets all facets for a given query
485+
486+ :returns: `dict` of facets
487+ """
488+
489+ facets_results = {}
490+
491+ for facet in self .facets :
492+ LOGGER .debug (f'Running facet for { facet } ' )
493+ facetq = self .session .query (self .query_mappings [facet ], self .func .count (facet )).group_by (facet )
494+
495+ if ast is not None :
496+ try :
497+ filters = to_filter (ast , self .dbtype , self .query_mappings )
498+ LOGGER .debug (f'Filter: { filters } ' )
499+ except Exception as err :
500+ msg = f'AST evaluator error: { str (err )} '
501+ LOGGER .exception (msg )
502+ raise RuntimeError (msg )
503+
504+ facetq = facetq .filter (filters )
505+
506+ LOGGER .debug ('Writing facet query results' )
507+ facets_results [facet ] = {
508+ 'type' : 'terms' ,
509+ 'property' : facet ,
510+ 'buckets' : []
511+ }
512+
513+ for fq in facetq .all ():
514+ facets_results [facet ]['buckets' ].append ({
515+ 'value' : fq [0 ],
516+ 'count' : fq [1 ]
517+ })
518+
519+ facets_results [facet ]['buckets' ].sort (key = itemgetter ('count' ), reverse = True )
520+
521+ return facets_results
522+
458523 def insert (self , record , source , insert_date ):
459524 ''' Insert a record into the repository '''
460525
0 commit comments