Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External resolver #711

Closed
wants to merge 81 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
5f710cd
adql query parser function
burnout87 Jun 24, 2024
5526e61
queryparser-python3 lib
burnout87 Jun 25, 2024
7d099aa
more error check
burnout87 Jun 25, 2024
fb87a08
adding info
burnout87 Jul 8, 2024
a90cc05
using sqlparse library
burnout87 Jul 18, 2024
ba08496
requirements
burnout87 Jul 18, 2024
6dcc1e2
using the two libraries combined
burnout87 Jul 18, 2024
eec215c
some where clauses extraction
burnout87 Jul 23, 2024
6759c7a
testing breadth-first
burnout87 Jul 26, 2024
0e24a34
Merge branch 'master' into ivoa_helper-endpoint
burnout87 Jul 29, 2024
90c8e4f
using breadth-first
burnout87 Jul 29, 2024
2c0030b
no sqlparse
burnout87 Jul 30, 2024
16580c2
querying mysql gallery database
burnout87 Jul 30, 2024
30d1ed8
todo and removed commented lines
burnout87 Jul 31, 2024
27708b2
vo options in the dispatcher config
burnout87 Jul 31, 2024
83d119a
extracting mysql parameters from config
burnout87 Jul 31, 2024
89f46d6
no need for breadth first search
burnout87 Jul 31, 2024
b828910
sentry in case of error
burnout87 Jul 31, 2024
34bdea8
capturing general exception
burnout87 Jul 31, 2024
1ee31a1
dispatcher endpoint
burnout87 Jul 31, 2024
40dc17e
build product gallery path and jsonify the response
burnout87 Jul 31, 2024
60c9bff
var renaming
burnout87 Aug 12, 2024
bdc61b4
removed unused imports
burnout87 Aug 16, 2024
3d8d973
Merge branch 'master' into ivoa_helper-endpoint
burnout87 Aug 16, 2024
841928f
Merge branch 'master' into ivoa_helper-endpoint
burnout87 Aug 16, 2024
b31c634
Merge branch 'master' into ivoa_helper-endpoint
burnout87 Sep 25, 2024
5342a7a
using postgresql
burnout87 Sep 26, 2024
bf1cd49
sanitize request values
burnout87 Sep 26, 2024
5d53425
using value var
burnout87 Sep 27, 2024
309e2ad
postgresql connector library
burnout87 Sep 27, 2024
08ad2ab
no query parsing
burnout87 Sep 27, 2024
ec8d018
not needed import
burnout87 Sep 27, 2024
f5df283
adapted conf example
burnout87 Sep 27, 2024
f9b934b
not needed requirements
burnout87 Sep 27, 2024
87256a9
not needed requirements
burnout87 Sep 27, 2024
db388d8
freezing version pytest-xdist
burnout87 Sep 27, 2024
d8d26a7
adapted config tests and new config test
burnout87 Sep 27, 2024
19c7275
in case local resolver fails, fallback to the external resolver
burnout87 Oct 2, 2024
a5826e0
extended configuration
burnout87 Oct 2, 2024
15b2e7a
handling not resolvable url
burnout87 Oct 2, 2024
5838b5b
extended tests
burnout87 Oct 2, 2024
72e67b1
adapted test
burnout87 Oct 2, 2024
cb6b75d
adql query parser function
burnout87 Jun 24, 2024
06dacd3
queryparser-python3 lib
burnout87 Jun 25, 2024
a4c616c
more error check
burnout87 Jun 25, 2024
7bbf6ab
adding info
burnout87 Jul 8, 2024
241b7b2
using sqlparse library
burnout87 Jul 18, 2024
11ae504
requirements
burnout87 Jul 18, 2024
c41e4e8
using the two libraries combined
burnout87 Jul 18, 2024
d7a9964
some where clauses extraction
burnout87 Jul 23, 2024
9fd948b
testing breadth-first
burnout87 Jul 26, 2024
201058b
using breadth-first
burnout87 Jul 29, 2024
37ed4fd
no sqlparse
burnout87 Jul 30, 2024
4084298
querying mysql gallery database
burnout87 Jul 30, 2024
61df4c9
todo and removed commented lines
burnout87 Jul 31, 2024
d4614d6
vo options in the dispatcher config
burnout87 Jul 31, 2024
a488f4c
extracting mysql parameters from config
burnout87 Jul 31, 2024
0ba6618
no need for breadth first search
burnout87 Jul 31, 2024
6b3fb19
sentry in case of error
burnout87 Jul 31, 2024
ed64d1c
capturing general exception
burnout87 Jul 31, 2024
4ff74d3
dispatcher endpoint
burnout87 Jul 31, 2024
60409db
build product gallery path and jsonify the response
burnout87 Jul 31, 2024
b35e2aa
var renaming
burnout87 Aug 12, 2024
b8fb588
removed unused imports
burnout87 Aug 16, 2024
0c42142
using postgresql
burnout87 Sep 26, 2024
f2fbc59
sanitize request values
burnout87 Sep 26, 2024
a660af0
using value var
burnout87 Sep 27, 2024
e995247
postgresql connector library
burnout87 Sep 27, 2024
d2c5acd
no query parsing
burnout87 Sep 27, 2024
081f08f
not needed import
burnout87 Sep 27, 2024
fb20a94
adapted conf example
burnout87 Sep 27, 2024
d1438aa
not needed requirements
burnout87 Sep 27, 2024
6b20e0d
not needed requirements
burnout87 Sep 27, 2024
900f223
freezing version pytest-xdist
burnout87 Sep 27, 2024
f7a1055
adapted config tests and new config test
burnout87 Sep 27, 2024
32f02fb
in case local resolver fails, fallback to the external resolver
burnout87 Oct 2, 2024
269ac50
extended configuration
burnout87 Oct 2, 2024
908c8ac
handling not resolvable url
burnout87 Oct 2, 2024
71e5187
extended tests
burnout87 Oct 2, 2024
4ef0339
adapted test
burnout87 Oct 2, 2024
aa398b7
Merge branch 'external-resolver' of github.com:oda-hub/dispatcher-app…
burnout87 Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 92 additions & 29 deletions cdci_data_analysis/analysis/drupal_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from enum import Enum, auto
from astropy.coordinates import SkyCoord, Angle
from astropy import units as u
import xml.etree.ElementTree as ET

from cdci_data_analysis.analysis import tokenHelper
from ..analysis.exceptions import RequestNotUnderstood, InternalError, RequestNotAuthorized
Expand Down Expand Up @@ -551,11 +552,14 @@ def post_content_to_gallery(decoded_token,
if update_astro_entity:
auto_update = kwargs.pop('auto_update', 'False') == 'True'
if auto_update is True:
name_resolver_url = disp_conf.name_resolver_url
local_name_resolver_url = disp_conf.local_name_resolver_url
external_name_resolver_url = disp_conf.external_name_resolver_url
entities_portal_url = disp_conf.entities_portal_url
resolved_obj = resolve_name(name_resolver_url=name_resolver_url,
resolved_obj = resolve_name(local_name_resolver_url=local_name_resolver_url,
external_name_resolver_url=external_name_resolver_url,
entities_portal_url=entities_portal_url,
name=src_name)
name=src_name,
sentry_dsn=sentry_dsn)
if resolved_obj is not None:
msg = ''
if 'message' in resolved_obj:
Expand Down Expand Up @@ -1488,39 +1492,98 @@ def check_matching_coords(source_1_name, source_1_coord_ra, source_1_coord_dec,
return False


def resolve_name(name_resolver_url: str, entities_portal_url: str = None, name: str = None):
def resolve_name(local_name_resolver_url: str, external_name_resolver_url: str, entities_portal_url: str = None, name: str = None, sentry_dsn=None):
resolved_obj = {}
if name is not None:
quoted_name = urllib.parse.quote(name.strip())
res = requests.get(name_resolver_url.format(quoted_name))
local_name_resolver_url_formatted = local_name_resolver_url.format(quoted_name)
try:
res = requests.get(local_name_resolver_url_formatted)
if res.status_code == 200:
returned_resolved_obj = res.json()
if 'success' in returned_resolved_obj:
resolved_obj['name'] = name.replace('_', ' ')
if returned_resolved_obj['success']:
logger.info(f"object {name} successfully resolved")
if 'ra' in returned_resolved_obj:
resolved_obj['RA'] = float(returned_resolved_obj['ra'])
if 'dec' in returned_resolved_obj:
resolved_obj['DEC'] = float(returned_resolved_obj['dec'])
if 'object_ids' in returned_resolved_obj:
resolved_obj['object_ids'] = returned_resolved_obj['object_ids']
if 'object_type' in returned_resolved_obj:
resolved_obj['object_type'] = returned_resolved_obj['object_type']
resolved_obj['entity_portal_link'] = entities_portal_url.format(quoted_name)
resolved_obj['message'] = f'{name} successfully resolved'
elif not returned_resolved_obj['success']:
logger.info(f"resolution of the object {name} unsuccessful")
resolved_obj['message'] = f'{name} could not be resolved'
else:
logger.warning("There seems to be some problem in completing the request for the resolution of the object"
f" \"{name}\" using the local resolver.\n"
f"The request lead to the error {res.text}, "
"this might be due to an error in the url or the service "
"requested is currently not available. The external resolver will be used.")
if sentry_dsn is not None:
sentry.capture_message(f'Failed to resolve object "{name}" using the local resolver. '
f'URL: {local_name_resolver_url_formatted} '
f'Status Code: {res.status_code} '
f'Response: {res.text}')
except (ConnectionError,
requests.exceptions.ConnectionError,
requests.exceptions.Timeout) as e:
logger.warning(f'An exception occurred while trying to resolve the object "{name}" using the local resolver. '
f'using the url: {local_name_resolver_url_formatted}. Exception details: {str(e)}')
if sentry_dsn is not None:
sentry.capture_message(f'An exception occurred while trying to resolve the object "{name}" using the local resolver. '
f'URL: {local_name_resolver_url_formatted} '
f"Exception details: {str(e)}")
res = requests.get(external_name_resolver_url.format(quoted_name))
if res.status_code == 200:
returned_resolved_obj = res.json()
if 'success' in returned_resolved_obj:
resolved_obj['name'] = name.replace('_', ' ')
if returned_resolved_obj['success']:
logger.info(f"object {name} successfully resolved")
if 'ra' in returned_resolved_obj:
resolved_obj['RA'] = float(returned_resolved_obj['ra'])
if 'dec' in returned_resolved_obj:
resolved_obj['DEC'] = float(returned_resolved_obj['dec'])
if 'object_ids' in returned_resolved_obj:
resolved_obj['object_ids'] = returned_resolved_obj['object_ids']
if 'object_type' in returned_resolved_obj:
resolved_obj['object_type'] = returned_resolved_obj['object_type']
resolved_obj['entity_portal_link'] = entities_portal_url.format(quoted_name)
resolved_obj['message'] = f'{name} successfully resolved'
elif not returned_resolved_obj['success']:
logger.info(f"resolution of the object {name} unsuccessful")
root = ET.fromstring(res.text)
resolved_obj['name'] = name.replace('_', ' ')
resolver_tag = root.find('.//Resolver')
if resolver_tag is not None:
ra_tag = resolver_tag.find('.//jradeg')
dec_tag = resolver_tag.find('.//jdedeg')
if ra_tag is None or dec_tag is None:
info_tag = root.find('.//INFO')
resolved_obj['message'] = f'{name} could not be resolved'
if info_tag is not None:
message_info = info_tag.text
resolved_obj['message'] += f': {message_info}'
else:
resolved_obj['RA'] = float(ra_tag.text)
resolved_obj['DEC'] = float(dec_tag.text)
resolved_obj['entity_portal_link'] = entities_portal_url.format(quoted_name)
else:
warning_msg = ("There seems to be some problem in completing the request for the resolution of the object"
f" \"{name}\" using the external resolver.")
resolved_obj['message'] = f'{name} could not be resolved'
info_tag = root.find('.//INFO')
if info_tag is not None:
warning_msg += (f"The request lead to the error {info_tag.text}, "
"this might be due to an error in the name of the object that ha been provided.")
resolved_obj['message'] += f': {info_tag.text}'
logger.warning(warning_msg)
if sentry_dsn is not None:
sentry.capture_message(f'Failed to resolve object "{name}" using the remote resolver. '
f'URL: {local_name_resolver_url.format(quoted_name)} '
f'Status Code: {res.status_code} '
f'Response: {res.text}'
f"Info returned from the resolver: {resolved_obj['message']}")
else:
logger.warning(f"there seems to be some problem in completing the request for the resolution of the object: {name}\n"
f"the request lead to the error {res.text}, "
logger.warning("There seems to be some problem in completing the request for the resolution of the object"
f" \"{name}\" using the external resolver.\n"
f"The request lead to the error {res.text}, "
"this might be due to an error in the url or the service "
"requested is currently not available, "
"please check your request and try to issue it again")
raise InternalError('issue when performing a request to the local resolver',
status_code=500,
payload={'drupal_helper_error_message': res.text})
"requested is currently not available. The object could not be resolved.")
if sentry_dsn is not None:
sentry.capture_message(f'Failed to resolve object "{name}" using the remote resolver. '
f'URL: {local_name_resolver_url.format(quoted_name)} '
f'Status Code: {res.status_code} '
f'Response: {res.text}')
resolved_obj['message'] = f'{name} could not be resolved: {res.text}'
return resolved_obj


Expand Down
93 changes: 93 additions & 0 deletions cdci_data_analysis/analysis/ivoa_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import os.path

from queryparser.adql import ADQLQueryTranslator
from queryparser.exceptions import QuerySyntaxError

from psycopg2 import connect, DatabaseError

from ..flask_app.sentry import sentry
from ..app_logging import app_logging

logger = app_logging.getLogger('ivoa_helper')


def parse_adql_query(query):
try:
adt = ADQLQueryTranslator(query)

output_obj = dict(
mysql_query=None,
psql_query=adt.to_postgresql()
)

except QuerySyntaxError as qe:
logger.error(f'Error parsing ADQL query: {qe}')
output_obj = dict(
mysql_query=None,
psql_query=None
)
return output_obj


def run_ivoa_query(query, **kwargs):
parsed_query_obj = parse_adql_query(query)

# TODO use a specific dedicated table and schema to refer to the product_gallery DB ?
# tables = parsed_query_obj.get('tables', [])
# if len(tables) == 1 and tables[0] == 'product_gallery':
logger.info('Performing query on the product_gallery')
vo_psql_pg_host = kwargs.get('vo_psql_pg_host', None)
vo_psql_pg_user = kwargs.get('vo_psql_pg_user', None)
vo_psql_pg_password = kwargs.get('vo_psql_pg_password', None)
vo_psql_pg_db = kwargs.get('vo_psql_pg_db', None)
product_gallery_url = kwargs.get('product_gallery_url', None)
result_list = run_ivoa_query_from_product_gallery(parsed_query_obj,
vo_psql_pg_host=vo_psql_pg_host,
vo_psql_pg_user=vo_psql_pg_user,
vo_psql_pg_password=vo_psql_pg_password,
vo_psql_pg_db=vo_psql_pg_db,
product_gallery_url=product_gallery_url)
return result_list


def run_ivoa_query_from_product_gallery(parsed_query_obj,
vo_psql_pg_host,
vo_psql_pg_user,
vo_psql_pg_password,
vo_psql_pg_db,
product_gallery_url=None
):
result_list = []

try:
with connect(
host=vo_psql_pg_host,
database=vo_psql_pg_db,
user=vo_psql_pg_user,
password=vo_psql_pg_password
) as connection:
db_query = parsed_query_obj.get('psql_query')
with connection.cursor() as cursor:
cursor.execute(db_query)
for row in cursor:
list_row = list(row)
if product_gallery_url is not None:
for index, value in enumerate(list_row):
description = cursor.description[index]
if description.name in {'path', 'path_alias'} and value is not None and isinstance(value, str):
if value.startswith('/'):
value = value[1:]
list_row[index] = os.path.join(product_gallery_url, value)
result_list.append(list_row)

except (Exception, DatabaseError) as e:
sentry.capture_message(f"Error when querying to the Postgresql server: {str(e)}")
logger.error(f"Error when querying to the Postgresql server: {str(e)}")

finally:
if connection is not None:
cursor.close()
connection.close()
logger.info('Database connection closed')

return result_list
19 changes: 16 additions & 3 deletions cdci_data_analysis/config_dir/conf_env.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,24 @@ dispatcher:
product_gallery_secret_key: PRODUCT_GALLERY_SECRET_KEY
# timezone used within the drupal configuration, these two values have to be always aligned
product_gallery_timezone: PRODUCT_GALLERY_SECRET_KEY
# url of the name resolver
name_resolver_url: NAME_RESOLVER_URL
# url of the local name resolver
local_name_resolver_url: NAME_RESOLVER_URL
# url of the external name resolver
external_name_resolver_url: NAME_RESOLVER_URL
# url of the online catalog for astrophysical entities
entities_portal_url: ENTITIES_PORTAL_URL
# url for the conversion of a given time, in UTC format, to the correspondent REVNUM
converttime_revnum_service_url: COVERTTIME_REVNUM_SERVICE_URL


# virtual observatory related configurations (eg mysql credentials)
vo_options:
# mysql credentials
vo_mysql_pg_host: MYSQL_PG_HOST
vo_mysql_pg_user: MYSQL_PG_USER
vo_mysql_pg_password: MYSQL_PG_PASSWORD
vo_mysql_pg_db: MYSQL_PG_DB
# postgresql credentials
vo_psql_pg_host: PSQL_PG_HOST
vo_psql_pg_user: PSQL_PG_USER
vo_psql_pg_password: PSQL_PG_PASSWORD
vo_psql_pg_db: PSQL_PG_DB
35 changes: 32 additions & 3 deletions cdci_data_analysis/configurer.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,23 @@ def __init__(self, cfg_dict, origin=None):
disp_dict.get('product_gallery_options', {}).get('product_gallery_secret_key', None),
disp_dict.get('product_gallery_options', {}).get('product_gallery_timezone',
"Europe/Zurich"),
disp_dict.get('product_gallery_options', {}).get('name_resolver_url', 'https://resolver-prod.obsuks1.unige.ch/api/v1.1/byname/{}'),
disp_dict.get('product_gallery_options', {}).get('local_name_resolver_url',
'https://resolver-prod.obsuks1.unige.ch/api/v1.1/byname/{}'),
disp_dict.get('product_gallery_options', {}).get('external_name_resolver_url',
'http://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame/-oxp/NSV?{}'),
disp_dict.get('product_gallery_options', {}).get('entities_portal_url', 'http://cdsportal.u-strasbg.fr/?target={}'),
disp_dict.get('product_gallery_options', {}).get('converttime_revnum_service_url', 'https://www.astro.unige.ch/mmoda/dispatch-data/gw/timesystem/api/v1.0/converttime/UTC/{}/REVNUM'),
disp_dict.get('renku_options', {}).get('renku_gitlab_repository_url', None),
disp_dict.get('renku_options', {}).get('renku_base_project_url', None),
disp_dict.get('renku_options', {}).get('ssh_key_path', None),
disp_dict.get('vo_options', {}).get('vo_mysql_pg_host', None),
disp_dict.get('vo_options', {}).get('vo_mysql_pg_user', None),
disp_dict.get('vo_options', {}).get('vo_mysql_pg_password', None),
disp_dict.get('vo_options', {}).get('vo_mysql_pg_db', None),
disp_dict.get('vo_options', {}).get('vo_psql_pg_host', None),
disp_dict.get('vo_options', {}).get('vo_psql_pg_user', None),
disp_dict.get('vo_options', {}).get('vo_psql_pg_password', None),
disp_dict.get('vo_options', {}).get('vo_psql_pg_db', None)
)

# not used?
Expand Down Expand Up @@ -338,12 +349,21 @@ def set_conf_dispatcher(self,
product_gallery_url,
product_gallery_secret_key,
product_gallery_timezone,
name_resolver_url,
local_name_resolver_url,
external_name_resolver_url,
entities_portal_url,
converttime_revnum_service_url,
renku_gitlab_repository_url,
renku_base_project_url,
renku_gitlab_ssh_key_path,
vo_mysql_pg_host,
vo_mysql_pg_user,
vo_mysql_pg_password,
vo_mysql_pg_db,
vo_psql_pg_host,
vo_psql_pg_user,
vo_psql_pg_password,
vo_psql_pg_db
):
# Generic to dispatcher
#print(dispatcher_url, dispatcher_port)
Expand Down Expand Up @@ -389,12 +409,21 @@ def set_conf_dispatcher(self,
self.product_gallery_url = product_gallery_url
self.product_gallery_secret_key = product_gallery_secret_key
self.product_gallery_timezone = product_gallery_timezone
self.name_resolver_url = name_resolver_url
self.local_name_resolver_url = local_name_resolver_url
self.external_name_resolver_url = external_name_resolver_url
self.entities_portal_url = entities_portal_url
self.converttime_revnum_service_url = converttime_revnum_service_url
self.renku_gitlab_repository_url = renku_gitlab_repository_url
self.renku_gitlab_ssh_key_path = renku_gitlab_ssh_key_path
self.renku_base_project_url = renku_base_project_url
self.vo_mysql_pg_host = vo_mysql_pg_host
self.vo_mysql_pg_user = vo_mysql_pg_user
self.vo_mysql_pg_password = vo_mysql_pg_password
self.vo_mysql_pg_db = vo_mysql_pg_db
self.vo_psql_pg_host = vo_psql_pg_host
self.vo_psql_pg_user = vo_psql_pg_user
self.vo_psql_pg_password = vo_psql_pg_password
self.vo_psql_pg_db = vo_psql_pg_db

def get_data_serve_conf(self, instr_name):
if instr_name in self.data_server_conf_dict.keys():
Expand Down
Loading
Loading