# Contains functions called by the top-level view functions # that process the user's request and return a rendered XML # template from typing import Optional from fastapi import Request, Query, Response from .enums import * from .diagnostics import Diagnostic from .config import ResourceConfig def initial_validation(operation, version, queryType, searchOptions, query): """ Validate and convert values of the main request parameters. Return converted values and a list of fatal diagnostics, if anything is wrong. """ failDiagnoctics = [] if version == '1.2': version = SRUVersion.v1_2 elif version == '2.0': version = SRUVersion.v2_0 else: version = SRUVersion.v2_0 failDiagnoctics.append(Diagnostic(DiagnosticTypes.sru, 5, details='2.0')) if operation == '': if len(query) > 0: operation = Operation.searchRetrieve else: operation = Operation.explain elif operation == 'explain': operation = Operation.explain elif operation == 'searchRetrieve': operation = Operation.searchRetrieve elif operation == 'scan': operation = Operation.scan else: operation = Operation.explain failDiagnoctics.append(Diagnostic(DiagnosticTypes.sru, 4, version=version)) if queryType == 'fcs': queryType = QueryType.fcs elif queryType == 'cql': queryType = QueryType.cql else: queryType = QueryType.cql failDiagnoctics.append(Diagnostic(DiagnosticTypes.sru, 6, message='Supported query types: fcs and cql.', version=version)) try: searchOptions['startRecord'] = int(searchOptions['startRecord']) except ValueError: searchOptions['startRecord'] = 1 failDiagnoctics.append(Diagnostic(DiagnosticTypes.sru, 6, message='startRecord should be a positive integer.', version=version)) if searchOptions['startRecord'] < 1: failDiagnoctics.append(Diagnostic(DiagnosticTypes.sru, 6, message='startRecord should be a positive integer.', version=version)) try: searchOptions['maximumRecords'] = int(searchOptions['maximumRecords']) except ValueError: searchOptions['maximumRecords'] = 0 failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 6, message='maximumRecords should be a non-negative integer.', version=version)) if searchOptions['maximumRecords'] < 0: failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 6, message='maximumRecords should be a non-negative integer.', version=version)) # recordPacking has entirely different semantics in SRU 1.2 and SRU 2.0 if version == SRUVersion.v1_2: if searchOptions['recordPacking'] == '': searchOptions['recordPacking'] = 'xml' if searchOptions['recordPacking'] not in ('xml', 'string'): failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 71, message='recordPacking should equal "xml" or "string".', version=version)) else: if searchOptions['recordXMLEscaping'] == '': searchOptions['recordXMLEscaping'] = 'xml' if searchOptions['recordPacking'] == '': searchOptions['recordPacking'] = 'packed' if searchOptions['recordXMLEscaping'] not in ('xml', 'string'): failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 71, message='recordXMLEscaping should equal "xml" or "string".', version=version)) if searchOptions['recordPacking'] not in ('packed', 'unpacked'): failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 6, message='recordPacking should equal "packed" or "unpacked".', version=version)) try: searchOptions['resultSetTTL'] = int(searchOptions['resultSetTTL']) except ValueError: searchOptions['resultSetTTL'] = 0 failDiagnoctics.append( Diagnostic(DiagnosticTypes.sru, 6, message='resultSetTTL should be a positive integer.', version=version)) if searchOptions['resultSetTTL'] < 0: # This does not look good, but we don't care because this # value is not used anyway pass # failDiagnoctics.append( # Diagnostic(DiagnosticTypes.sru, 6, message='resultSetTTL should be a positive integer.', # version=version)) return operation, version, queryType, searchOptions, failDiagnoctics def fatal_response(operation: Operation, version: SRUVersion, config: Optional[ResourceConfig], diagnostics: list[Diagnostic], request, templates): """ Return a response with the fatal diagnostics and no other payload. """ if config is None: configStr = '' else: configStr = config.as_dict() if version == SRUVersion.v1_2: templateVersion = 1 else: templateVersion = 2 for diag in diagnostics: diag.version = version diagStr = [str(d) for d in diagnostics] if operation in (Operation.explain, Operation.scan): return templates.TemplateResponse('explain_response.xml', { 'request': request, 'diagnostics': diagStr, 'config': configStr, 'version': templateVersion }, media_type='application/xml') elif operation == Operation.searchRetrieve: return templates.TemplateResponse('search_retrieve_response.xml', { 'request': request, 'diagnostics': diagStr, 'n_hits': 0, 'version': templateVersion }, media_type='application/xml') def process_explain(version: SRUVersion, searchOptions: dict[str, str], config: Optional[ResourceConfig], diagnostics: list[Diagnostic], request, templates): """ Process an explain request. Return a rendered XML response. """ if version == SRUVersion.v1_2: templateVersion = 1 else: templateVersion = 2 for diag in diagnostics: diag.version = version endpointDescNeeded = False if 'x-fcs-endpoint-description' in searchOptions and searchOptions['x-fcs-endpoint-description'] == 'true': endpointDescNeeded = True diagStr = [str(d) for d in diagnostics] return templates.TemplateResponse('explain_response.xml', { 'request': request, 'diagnostics': diagStr, 'config': config.as_dict(), 'version': templateVersion, 'endpoint_desc_needed': endpointDescNeeded }, media_type='application/xml') def process_search_retrieve(version: SRUVersion, queryType: QueryType, query: str, searchOptions: dict, config: Optional[ResourceConfig], diagnostics: list[Diagnostic], app, request, templates): """ Process a searchRetrieve request. Return a rendered XML response. """ if version == SRUVersion.v1_2: templateVersion = 1 else: templateVersion = 2 for diag in diagnostics: diag.version = version if config.platform == CorpPlatform.annis: try: if queryType == QueryType.cql: query = app.qp_annis.translate_simple(query, config, searchOptions) else: query = app.qp_annis.translate_advanced(query, config, searchOptions) print(query) res = app.qp_annis.send_query(query, config) except Diagnostic as diag: return fatal_response(Operation.searchRetrieve, version, config, diagnostics + [diag], request, templates) # return query['query'] return res # records, nHits, diagnostics = app.rp_annis.parse(res, config, searchOptions['x-fcs-dataviews']) # if any(diag.is_fatal() for diag in diagnostics): # return fatal_response(Operation.searchRetrieve, version, config, diagnostics, request, templates) # records = [r.as_dict() for r in records] # diagnostics = [str(d) for d in diagnostics] # return templates.TemplateResponse('search_retrieve_response.xml', # { # 'request': request, # 'n_hits': nHits, # 'records': records, # 'version': templateVersion, # 'diagnostics': diagnostics # }, # media_type='application/xml') if config.platform == CorpPlatform.tsakorpus: try: if queryType == QueryType.cql: strGetParams = app.qp_tsakorpus.translate_simple(query, config, searchOptions) else: strGetParams = app.qp_tsakorpus.translate_advanced(query, config, searchOptions) print(strGetParams) res = app.qp_tsakorpus.send_query(strGetParams, config) except Diagnostic as diag: return fatal_response(Operation.searchRetrieve, version, config, diagnostics + [diag], request, templates) records, nHits, diagnostics = app.rp_tsakorpus.parse(res, config, searchOptions) if any(diag.is_fatal() for diag in diagnostics): return fatal_response(Operation.searchRetrieve, version, config, diagnostics, request, templates) records = [r.as_dict() for r in records] diagnostics = [str(d) for d in diagnostics] return templates.TemplateResponse('search_retrieve_response.xml', { 'request': request, 'n_hits': nHits, 'records': records, 'version': templateVersion, 'diagnostics': diagnostics }, media_type='application/xml') elif config.platform == CorpPlatform.litterae: try: if queryType == QueryType.cql: strGetParams = app.qp_litterae.translate_simple(query, config, searchOptions) else: # No advanced search for Litterae strGetParams = app.qp_litterae.translate_simple(query, config, searchOptions) # print(strGetParams) res = app.qp_litterae.send_query(strGetParams, config) print(res) except Diagnostic as diag: return fatal_response(Operation.searchRetrieve, version, config, diagnostics + [diag], request, templates) for dv in searchOptions['x-fcs-dataviews'].split(','): dv = dv.strip() if dv != 'hits' and version == SRUVersion.v2_0: # Litterae does not provide any additional annotation, so only Generic Hits # are available as a data view. # If SRU 1.2 is used, such a diagnostic has already been added # at a previous step. diagnostics.append(Diagnostic(DiagnosticTypes.fcs, 4, details=dv, version=version)) records, nHits, diagnostics = app.rp_litterae.parse(res, config, searchOptions) if any (diag.is_fatal() for diag in diagnostics): return fatal_response(Operation.searchRetrieve, version, config, diagnostics, request, templates) records = [r.as_dict() for r in records] diagnostics = [str(d) for d in diagnostics] return templates.TemplateResponse('search_retrieve_response.xml', { 'request': request, 'n_hits': nHits, 'records': records, 'version': templateVersion, 'diagnostics': diagnostics }, media_type='application/xml') def process_request(operation: Operation, version: SRUVersion, queryType: QueryType, query: str, searchOptions: dict[str, str], config: Optional[ResourceConfig], diagnostics: list[Diagnostic], app, request, templates): """ Process validated user request that came in through the endpoint() function in main.py. Return a rendered template. :param diagnostics: List of diagnostics produced by the validation function. """ print(query) # If something is clearly wrong with the query, return # a response with the list of diagnostics if config is None or any(d.is_fatal() for d in diagnostics): return fatal_response(operation, version, config, diagnostics, request, templates) # If everything looks good, proceed to query parsing if operation == Operation.searchRetrieve: return process_search_retrieve(version, queryType, query, searchOptions, config, diagnostics, app, request, templates) elif operation == Operation.explain: return process_explain(version, searchOptions, config, diagnostics, request, templates) # We should not end up here, but if we did, something went wrong and # no fatal diagnostic describes the problem. Add a generic fatal diagnostic # and return a fatal response. diagnostics.append(Diagnostic(DiagnosticTypes.sru, 1, version=version)) return fatal_response(operation, version, config, diagnostics, request, templates) if __name__ == '__main__': pass