diff --git a/common/config.py b/common/config.py new file mode 100644 index 0000000000000000000000000000000000000000..370b108732704ae34ddcc3333ed2e569499defea --- /dev/null +++ b/common/config.py @@ -0,0 +1,170 @@ +""" +Contains a class that handles endpoint configuration. Its main +functions are reading the configuration files and restoring the +defaults if some keys are absent there. +""" +from .enums import * +import json +import copy +import re +import os + + +class ResourceConfig: + """ + Properties of this class correspond to the keys in a configuration file + for one of the resources this endpoint communicates with. + """ + + def __init__(self, fnameConfig=None): + self.platform = CorpPlatform.annis + self.corpus_id = '' + self.transport_protocol = 'https' + self.host = '127.0.0.1' + self.port = '5000' + self.url_path = '127.0.0.1' + self.titles = [] + self.descriptions = [] + self.authors = [] + self.contacts = [] + self.extents = [] + self.history = [] + self.restrictions = [] + self.max_hits = 10 + self.basic_search_capability = True + self.advanced_search_capability = False + self.hits_supported = True + self.adv_supported = False + self.supported_layers = [] + self.resources = [] + + self.query_timeout = 60 + + self.boolParams = set(k for k in self.__dict__ + if type(self.__dict__[k]) == bool) + self.intParams = set(k for k in self.__dict__ + if type(self.__dict__[k]) == int) + self.lsParams = {} + + # dictionaries where values are strings + self.dict_sParams = {} + + # dictionaries where values are lists of strings + self.dict_lsParams = {} + + # dictionaries where values are dictionaries {k: string} + self.dict_dParams = {} + + if fnameConfig is not None and os.path.exists(fnameConfig): + self.load_settings(fnameConfig) + + + def load_settings(self, fnameConfig): + """ + Load configuration for one of the resources from a JSON file. + """ + with open(fnameConfig, 'r', encoding='utf-8') as fConfig: + config = json.load(fConfig) + for k, v in config.items(): + setattr(self, k, v) + + def as_dict(self): + """ + Return current settings as a dictionary. + """ + dictSettings = copy.deepcopy(vars(self)) + for k in [_ for _ in dictSettings.keys()]: + if dictSettings[k] is None: + dictSettings[k] = '' + return dictSettings + + def gui_str_to_dict(self, s, value_type='list'): + """ + Process one input string that describes a dictionary. + """ + d = {} + s = s.replace('\r', '').strip() + s = re.sub('\n\n+', '\n', s, flags=re.DOTALL) + if value_type == 'dict': + prevKey = '' + curData = {} + for line in s.split('\n'): + if not line.startswith(' '): + curKey = line.strip(': ') + if len(prevKey) > 0 and curKey != prevKey: + d[prevKey] = curData + curData = {} + prevKey = curKey + else: + line = line.strip() + if ':' not in line: + continue + k, v = line.split(':') + k = k.rstrip() + v = v.lstrip() + curData[k] = v + if len(curData) > 0: + d[prevKey] = curData + else: + for line in s.split('\n'): + line = line.strip() + if ':' not in line: + continue + k, v = line.split(':') + k = k.rstrip() + v = v.lstrip() + if value_type == 'list': + if len(v) <= 0: + v = [] + else: + v = [vp.strip() for vp in v.split(',')] + d[k] = v + return d + + def processed_gui_config(self, data): + """ + Turn form data filled by the user in the configuration GUI to + a dictionary in the correct format. + """ + dictConfig = {} + for f in self.boolParams: + if f in data and len(data[f]) > 0: + dictConfig[f] = True + else: + dictConfig[f] = False + for f in self.intParams: + if f in data and len(data[f]) > 0: + dictConfig[f] = int(data[f]) + for f in self.lsParams: + if f in data and len(data[f]) > 0: + dictConfig[f] = [v.strip() for v in data[f].replace('\r', '').strip().split('\n')] + else: + dictConfig[f] = [] + for f in self.dict_sParams: + if f in data and len(data[f]) > 0: + dictConfig[f] = self.gui_str_to_dict(data[f], value_type='string') + else: + dictConfig[f] = {} + for f in self.dict_lsParams: + if f in data and len(data[f]) > 0: + dictConfig[f] = self.gui_str_to_dict(data[f], value_type='list') + else: + dictConfig[f] = {} + for k, v in data.items(): + if '%' in k: + continue + if k not in dictConfig: + dictConfig[k] = v + return dictConfig + + def save_settings(self, fnameOut, data=None): + """ + Save current or new configuration as a JSON file (can be used to edit + configuration files through a web interface). + """ + if data is None or len(data) <= 0: + dictConfig = self.as_dict() + else: + dictConfig = self.processed_gui_config(data) + with open(fnameOut, 'w', encoding='utf-8') as fOut: + json.dump(dictConfig, fOut, sort_keys=True, ensure_ascii=False, indent=2) diff --git a/config/test.json b/config/test.json new file mode 100644 index 0000000000000000000000000000000000000000..1b9ba57aa4866e3a5322d2415e02d69a55ae997b --- /dev/null +++ b/config/test.json @@ -0,0 +1,5 @@ +{ + "host": "0.0.0.0", + "port": "80", + "max_hits": 20 +} \ No newline at end of file diff --git a/main.py b/main.py index 7af51bddf2f96e3345e8b0667ec52af82bb919ac..d4b3b2900bcb6995893a5fc11ca47d2bc63dbc83 100644 --- a/main.py +++ b/main.py @@ -6,15 +6,28 @@ from fastapi.responses import JSONResponse from common.query_parser import QueryParser from common.enums import * from common.diagnostics import Diagnostic +from common.config import ResourceConfig import json +import os +import re import uvicorn +rxExt = re.compile('\\.[^.]*$') app = FastAPI() app.mount('/static', StaticFiles(directory='static'), name='static') templates = Jinja2Templates(directory='static') app.qp = QueryParser() +app.configs = {} +i = 0 +for fname in os.listdir('config'): + if not fname.lower().endswith('.json'): + continue + i += 1 + fnameFull = os.path.join('config', fname) + fnameNoExt = rxExt.sub('', fname) + app.configs[fnameNoExt] = ResourceConfig(fnameFull) @app.get('/') diff --git a/static/endpoint_description.xml b/static/endpoint_description.xml index 2aac1cb9f8665bba0f38fc22bd551d5e1bdff3fc..2ec603d0d11c7e9201d9d3a2d21f478fccb2807c 100644 --- a/static/endpoint_description.xml +++ b/static/endpoint_description.xml @@ -1,18 +1,18 @@ <sru:extraResponseData> <ed:EndpointDescription xmlns:ed="http://clarin.eu/fcs/endpoint-description" version="{{ ep_version }}"> - <ed:Capabilities>{% if capability_basic_search %} - <ed:Capability>http://clarin.eu/fcs/capability/basic-search</ed:Capability>{% endif %}{% if capability_advanced_search and ep_version >= 2 %} + <ed:Capabilities>{% if config.basic_search_capability %} + <ed:Capability>http://clarin.eu/fcs/capability/basic-search</ed:Capability>{% endif %}{% if config.advanced_search_capability and ep_version >= 2 %} <ed:Capability>http://clarin.eu/fcs/capability/advanced-search</ed:Capability>{% endif %} </ed:Capabilities> - <ed:SupportedDataViews>{% if hits_supported %} - <ed:SupportedDataView id="hits" delivery-policy="send-by-default">application/x-clarin-fcs-hits+xml</ed:SupportedDataView>{% endif %}{% if adv_supported and ep_version >= 2%} + <ed:SupportedDataViews>{% if config.hits_supported %} + <ed:SupportedDataView id="hits" delivery-policy="send-by-default">application/x-clarin-fcs-hits+xml</ed:SupportedDataView>{% endif %}{% if config.adv_supported and ep_version >= 2%} <ed:SupportedDataView id="adv" delivery-policy="send-by-default">application/x-clarin-fcs-adv+xml</ed:SupportedDataView>{% endif %} - </ed:SupportedDataViews>{% if capability_advanced_search and ep_version >= 2 %} - <ed:SupportedLayers>{% for layer in supported_layers %} + </ed:SupportedDataViews>{% if config.advanced_search_capability and ep_version >= 2 %} + <ed:SupportedLayers>{% for layer in config.supported_layers %} <ed:SupportedLayer id="{{ layer.id }}" result-id="{{ layer.result_id }}"{% if layer.alt_value_info %} alt-value-info="{{ layer.alt_value_info }}"{% endif %}{% if layer.alt_value_info_uri %} alt-value-info-uri="{{ layer.alt_value_info_uri }}"{% endif %}{% if layer.qualifier %} qualifier="{{ layer.qualifier }}"{% endif %}>{{ layer.layer_type }}</ed:SupportedLayer>{% endfor %} </ed:SupportedLayers> {% endif %} - <ed:Resources>{% for r in resources %} + <ed:Resources>{% for r in config.resources %} <ed:Resource pid="{{ r.pid }}">{% for title in r.titles %} <ed:Title xml:lang="{{ title.lang }}">{{ title.content }}</ed:Title>{% endfor %}{% for desc in r.descriptions %} <ed:Description xml:lang="{{ desc.lang }}">{{ desc.content }}</ed:Description>{% if landing_page|length > 0%} diff --git a/static/explain_response_1.2.xml b/static/explain_response_1.2.xml index 0b80d08fa7b1460f406cc91182feca45fa8d5387..3462e1d5af9b1078cd0c8bcb88a88dc818e52185 100644 --- a/static/explain_response_1.2.xml +++ b/static/explain_response_1.2.xml @@ -9,18 +9,18 @@ <zr:explain xmlns:zr="http://explain.z3950.org/dtd/2.0/"> <!-- <zr:serverInfo > is REQUIRED --> <zr:serverInfo protocol="SRU" version="1.2" transport="{{ transport_protocol }}" method="GET"> - <zr:host>{{ host }}</zr:host> - <zr:port>{{ port }}</zr:port> - <zr:database>{{ url_path }}/fcs-endpoint/{{ platform }}/{{ corpus_id }}</zr:database> + <zr:host>{{ config.host }}</zr:host> + <zr:port>{{ config.port }}</zr:port> + <zr:database>{{ config.url_path }}/fcs-endpoint/{{ config.platform }}/{{ config.corpus_id }}</zr:database> </zr:serverInfo> <!-- <zr:databaseInfo> is REQUIRED --> - <zr:databaseInfo>{% for title in titles %} - <zr:title lang="{{ title.lang }}"{% if title.primary %} primary="true"{% endif %}>{{ title.content }}</zr:title>{% endfor %}{% for desc in descriptions %} - <zr:description lang="{{ desc.lang }}"{% if desc.primary %} primary="true"{% endif %}>{{ desc.content }}</zr:description>{% endfor %}{% for author in authors %} - <zr:author lang="{{ author.lang }}"{% if author.primary %} primary="true"{% endif %}>{{ author.content }}</zr:author>{% endfor %}{% for contact in contacts %} - <zr:contact lang="{{ contact.lang }}"{% if contact.primary %} primary="true"{% endif %}>{{ contact.content }}</zr:contact>{% endfor %}{% for extent in extents %} - <zr:extent lang="{{ extent.lang }}"{% if extent.primary %} primary="true"{% endif %}>{{ extent.content }}</zr:extent>{% endfor %}{% for hist in history %} - <zr:history lang="{{ hist.lang }}"{% if hist.primary %} primary="true"{% endif %}>{{ hist.content }}</zr:history>{% endfor %}{% for restr in restrictions %} + <zr:databaseInfo>{% for title in config.titles %} + <zr:title lang="{{ title.lang }}"{% if title.primary %} primary="true"{% endif %}>{{ title.content }}</zr:title>{% endfor %}{% for desc in config.descriptions %} + <zr:description lang="{{ desc.lang }}"{% if desc.primary %} primary="true"{% endif %}>{{ desc.content }}</zr:description>{% endfor %}{% for author in config.authors %} + <zr:author lang="{{ author.lang }}"{% if author.primary %} primary="true"{% endif %}>{{ author.content }}</zr:author>{% endfor %}{% for contact in config.contacts %} + <zr:contact lang="{{ contact.lang }}"{% if contact.primary %} primary="true"{% endif %}>{{ contact.content }}</zr:contact>{% endfor %}{% for extent in config.extents %} + <zr:extent lang="{{ extent.lang }}"{% if extent.primary %} primary="true"{% endif %}>{{ extent.content }}</zr:extent>{% endfor %}{% for hist in config.history %} + <zr:history lang="{{ hist.lang }}"{% if hist.primary %} primary="true"{% endif %}>{{ hist.content }}</zr:history>{% endfor %}{% for restr in config.restrictions %} <zr:restrictions lang="{{ restr.lang }}"{% if restr.primary %} primary="true"{% endif %}>{{ restr.content }}</zr:restrictions>{% endfor %} </zr:databaseInfo> <!-- <zr:schemaInfo> is REQUIRED --> @@ -28,10 +28,10 @@ <zr:schema identifier="http://clarin.eu/fcs/resource" name="fcs"> <zr:title lang="en" primary="true">CLARIN Federated Content Search</zr:title> </zr:schema> - </zr:schemaInfo>{% if max_hits > 0 %} + </zr:schemaInfo>{% if config.max_hits > 0 %} <!-- <zr:configInfo> is OPTIONAL --> <zr:configInfo> - <zr:setting type="maximumRecords">{{ max_hits }}</zr:setting> + <zr:setting type="maximumRecords">{{ config.max_hits }}</zr:setting> </zr:configInfo>{% endif %} </zr:explain> </sru:recordData>