diff --git a/prez/app.py b/prez/app.py index 2d22cb5a..0cd2b0e8 100755 --- a/prez/app.py +++ b/prez/app.py @@ -1,4 +1,5 @@ import logging +from contextlib import asynccontextmanager from functools import partial from textwrap import dedent from typing import Optional, Dict, Union, Any @@ -85,56 +86,49 @@ async def add_cors_headers(request, call_next): return response -async def app_startup(_settings: Settings, _app: FastAPI): - """ - This function runs at startup and will continually poll the separate backends until their SPARQL endpoints - are available. Initial caching can be triggered within the try block. NB this function does not check that data is - appropriately configured at the SPARQL endpoint(s), only that the SPARQL endpoint(s) are reachable. - """ - setup_logger(_settings) +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + setup_logger(app.state.settings) log = logging.getLogger("prez") log.info("Starting up") - if _settings.sparql_repo_type == "pyoxigraph": - _app.state.pyoxi_store = get_pyoxi_store() - _app.state.repo = _repo = PyoxigraphRepo(_app.state.pyoxi_store) - await load_local_data_to_oxigraph(_app.state.pyoxi_store) - elif _settings.sparql_repo_type == "oxrdflib": - _app.state.oxrdflib_store = get_oxrdflib_store() - _app.state.repo = _repo = OxrdflibRepo(_app.state.oxrdflib_store) - elif _settings.sparql_repo_type == "remote": - _app.state.http_async_client = await get_async_http_client() - _app.state.repo = _repo = RemoteSparqlRepo(_app.state.http_async_client) + if app.state.settings.sparql_repo_type == "pyoxigraph": + app.state.pyoxi_store = get_pyoxi_store() + app.state.repo = PyoxigraphRepo(app.state.pyoxi_store) + await load_local_data_to_oxigraph(app.state.pyoxi_store) + elif app.state.settings.sparql_repo_type == "oxrdflib": + app.state.oxrdflib_store = get_oxrdflib_store() + app.state.repo = OxrdflibRepo(app.state.oxrdflib_store) + elif app.state.settings.sparql_repo_type == "remote": + app.state.http_async_client = await get_async_http_client() + app.state.repo = RemoteSparqlRepo(app.state.http_async_client) await healthcheck_sparql_endpoints() else: raise ValueError( "SPARQL_REPO_TYPE must be one of 'pyoxigraph', 'oxrdflib' or 'remote'" ) - await prefix_initialisation(_repo) - await retrieve_remote_template_queries(_repo) - await create_profiles_graph(_repo) - await create_endpoints_graph(_repo) - await count_objects(_repo) + await prefix_initialisation(app.state.repo) + await retrieve_remote_template_queries(app.state.repo) + await create_profiles_graph(app.state.repo) + await create_endpoints_graph(app.state.repo) + await count_objects(app.state.repo) await populate_api_info() - _app.state.pyoxi_system_store = get_system_store() - _app.state.annotations_store = get_annotations_store() - await load_system_data_to_oxigraph(_app.state.pyoxi_system_store) - await load_annotations_data_to_oxigraph(_app.state.annotations_store) + app.state.pyoxi_system_store = get_system_store() + app.state.annotations_store = get_annotations_store() + await load_system_data_to_oxigraph(app.state.pyoxi_system_store) + await load_annotations_data_to_oxigraph(app.state.annotations_store) + yield -async def app_shutdown(_settings: Settings, _app: FastAPI): - """ - persists caches - close async SPARQL clients - """ - log = logging.getLogger("prez") + # Shutdown log.info("Shutting down...") # close all SPARQL async clients - if not _settings.sparql_repo_type: - await _app.state.http_async_client.aclose() + if app.state.settings.sparql_repo_type == "remote": + await app.state.http_async_client.aclose() def assemble_app( @@ -145,7 +139,6 @@ def assemble_app( local_settings: Optional[Settings] = None, **kwargs ): - _settings = local_settings if local_settings is not None else settings if title is None: @@ -162,6 +155,7 @@ def assemble_app( version=version, description=description, contact=contact, + lifespan=lifespan, exception_handlers={ 400: catch_400, 404: catch_404, @@ -175,6 +169,8 @@ def assemble_app( **kwargs ) + app.state.settings = _settings + app.include_router(management_router) app.include_router(ogc_records_router) if _settings.enable_sparql_endpoint: @@ -203,8 +199,7 @@ def assemble_app( allow_headers=["*"], expose_headers=["*"], ) - app.on_event("startup")(partial(app_startup, _settings=_settings, _app=app)) - app.on_event("shutdown")(partial(app_shutdown, _settings=_settings, _app=app)) + return app diff --git a/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.json b/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.json new file mode 100644 index 00000000..3506dfd7 --- /dev/null +++ b/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.json @@ -0,0 +1,1284 @@ +{ + "t_after": { + "definition": [ + "https://www.w3.org/TR/owl-time/#time:after", + ",https://www.w3.org/TR/owl-time/#time:intervalAfter" + ], + "interval_interval": { + "BB_BB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_BU": false, + "BU_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + }, + "instant_interval": { + "I_BB": { + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ] + ] + }, + "I_BU": false, + "I_UB": { + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ] + ] + }, + "I_UU": false + }, + "interval_instant": { + "BB_I": { + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ] + ] + }, + "BU_I": { + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ] + ] + }, + "UB_I": false, + "UU_I": false + }, + "instant_instant": { + "I_I": { + "conditions": [ + [ + "t1_instant", + ">", + "t2_instant" + ] + ] + } + } + }, + "t_before": { + "definition": [ + "https://www.w3.org/TR/owl-time/#time:before", + "https://www.w3.org/TR/owl-time/#time:intervalBefore" + ], + "interval_interval": { + "BB_BB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "BB_BU": false, + "BB_UB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "BU_BU": false, + "BU_UB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + }, + "instant_interval": { + "I_BB": { + "conditions": [ + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_BU": false, + "I_UB": { + "conditions": [ + [ + "t1_instant", + "<", + "t2_end" + ] + ] + }, + "I_UU": false + }, + "interval_instant": { + "BB_I": false, + "BU_I": false, + "UB_I": { + "conditions": [ + [ + "t1_start", + "<", + "t2_instant" + ] + ] + }, + "UU_I": { + "conditions": [ + [ + "t1_start", + "<", + "t2_instant" + ] + ] + } + }, + "instant_instant": { + "I_I": { + "conditions": [ + [ + "t1_instant", + "<", + "t2_instant" + ] + ] + } + } + }, + "t_disjoint": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalDisjoint", + "interval_interval": { + "BB_BB": { + "logic": "OR", + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ], + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_BU": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "BB_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_BU": false, + "BU_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_UU": false, + "UB_BB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "UB_BU": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + }, + "instant_interval": { + "I_BB": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_BU": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_UB": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_UU": false + }, + "interval_instant": { + "BB_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "BU_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "UB_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "UU_I": false + }, + "instant_instant": { + "I_I": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_instant" + ], + [ + "t1_instant", + "<", + "t2_instant" + ] + ] + } + } + }, + "t_equals": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalEquals", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ], + [ + "t1_end", + "=", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": false, + "BB_UU": false, + "BU_BB": false, + "BU_BU": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ] + ] + }, + "BU_UB": false, + "BU_UU": false, + "UB_BB": false, + "UB_BU": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ] + ] + }, + "UB_UB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ] + ] + }, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": true + }, + "instant_instant": { + "I_I": { + "logic": "AND", + "conditions": [ + [ + "t1_instant", + "=", + "t2_instant" + ] + ] + } + }, + "instant_interval": { + "I_BB": false, + "I_BU": false, + "I_UB": false, + "I_UU": false + }, + "interval_instant": { + "BB_I": false, + "BU_I": false, + "UB_I": false, + "UU_I": false + } + }, + "t_intersects": { + "negated": true, + "definition": "https://www.w3.org/TR/owl-time/#time:intervalDisjoint", + "interval_interval": { + "BB_BB": { + "logic": "OR", + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ], + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_BU": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "BB_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_BU": false, + "BU_UB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_end" + ] + ] + }, + "BU_UU": false, + "UB_BB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "UB_BU": { + "conditions": [ + [ + "t1_end", + "<", + "t2_start" + ] + ] + }, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + }, + "instant_interval": { + "I_BB": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_BU": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_UB": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_end" + ], + [ + "t1_instant", + "<", + "t2_start" + ] + ] + }, + "I_UU": false + }, + "interval_instant": { + "BB_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "BU_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "UB_I": { + "logic": "OR", + "conditions": [ + [ + "t1_start", + ">", + "t2_instant" + ], + [ + "t1_end", + "<", + "t2_instant" + ] + ] + }, + "UU_I": false + }, + "instant_instant": { + "I_I": { + "logic": "OR", + "conditions": [ + [ + "t1_instant", + ">", + "t2_instant" + ], + [ + "t1_instant", + "<", + "t2_instant" + ] + ] + } + } + }, + "t_contains": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalContains", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": false, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ] + ] + }, + "BU_BU": { + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ] + ] + }, + "BU_UB": false, + "BU_UU": false, + "UB_BB": { + "conditions": [ + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "UB_BU": false, + "UB_UB": { + "logic": "AND", + "conditions": [ + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "UB_UU": false, + "UU_BB": true, + "UU_BU": true, + "UU_UB": true, + "UU_UU": false + } + }, + "t_during": { + "definition": "Logical inverse of https://www.w3.org/TR/owl-time/#time:intervalContains", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ], + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "BB_BU": true, + "BB_UB": true, + "BB_UU": true, + "BU_BB": { + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ] + ] + }, + "BU_BU": { + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ] + ] + }, + "BU_UB": true, + "BU_UU": true, + "UB_BB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "UB_BU": true, + "UB_UB": { + "conditions": [ + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "UB_UU": true, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_finishes": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalFinishes", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ], + [ + "t1_end", + "=", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": { + "conditions": [ + [ + "t1_end", + "=", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": false, + "BU_BU": false, + "BU_UB": false, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_finishedBy": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalFinishedBy", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + "=", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": false, + "BB_UU": false, + "BU_BB": false, + "BU_BU": false, + "BU_UB": false, + "BU_UU": false, + "UB_BB": { + "conditions": [ + [ + "t1_end", + "=", + "t2_end" + ] + ] + }, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_meets": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalMeets", + "interval_interval": { + "BB_BB": { + "conditions": [ + [ + "t1_end", + "=", + "t2_start" + ] + ] + }, + "BB_BU": { + "conditions": [ + [ + "t1_end", + "=", + "t2_start" + ] + ] + }, + "BB_UB": false, + "BB_UU": false, + "BU_BB": false, + "BU_BU": false, + "BU_UB": false, + "BU_UU": false, + "UB_BB": { + "conditions": [ + [ + "t1_end", + "=", + "t2_start" + ] + ] + }, + "UB_BU": { + "conditions": [ + [ + "t1_end", + "=", + "t2_start" + ] + ] + }, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_metBy": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalMetBy", + "interval_interval": { + "BB_BB": { + "conditions": [ + [ + "t1_start", + "=", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": { + "conditions": [ + [ + "t1_start", + "=", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + "=", + "t2_end" + ] + ] + }, + "BU_BU": false, + "BU_UB": { + "conditions": [ + [ + "t1_start", + "=", + "t2_end" + ] + ] + }, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_overlappedBy": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalOverlappedBy", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ], + [ + "t1_start", + "<", + "t2_end" + ], + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_end" + ], + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "BB_UU": false, + "BU_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ], + [ + "t1_start", + "<", + "t2_end" + ] + ] + }, + "BU_BU": { + "conditions": [ + [ + "t1_start", + ">", + "t2_start" + ] + ] + }, + "BU_UB": false, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_overlaps": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalOverlaps", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_start" + ], + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "BB_BU": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_start" + ] + ] + }, + "BB_UB": false, + "BB_UU": false, + "BU_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_start" + ] + ] + }, + "BU_BU": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "<", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_start" + ] + ] + }, + "BU_UB": false, + "BU_UU": false, + "UB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_end", + ">", + "t2_start" + ], + [ + "t1_end", + "<", + "t2_end" + ] + ] + }, + "UB_BU": { + "conditions": [ + [ + "t1_end", + ">", + "t2_start" + ] + ] + }, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_startedBy": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalStartedBy", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ], + [ + "t1_end", + ">", + "t2_end" + ] + ] + }, + "BB_BU": false, + "BB_UB": false, + "BB_UU": false, + "BU_BB": { + "conditions": [ + [ + "t1_start", + "=", + "t2_start" + ] + ] + }, + "BU_BU": false, + "BU_UB": false, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + }, + "t_starts": { + "definition": "https://www.w3.org/TR/owl-time/#time:intervalStarts", + "interval_interval": { + "BB_BB": { + "logic": "AND", + "conditions": [ + ["t1_start", "=", "t2_start"], + ["t1_end", "<", "t2_end"] + ] + }, + "BB_BU": { + "conditions": [ + ["t1_start", "=", "t2_start"] + ] + }, + "BB_UB": false, + "BB_UU": false, + "BU_BB": false, + "BU_BU": false, + "BU_UB": false, + "BU_UU": false, + "UB_BB": false, + "UB_BU": false, + "UB_UB": false, + "UB_UU": false, + "UU_BB": false, + "UU_BU": false, + "UU_UB": false, + "UU_UU": false + } + } +} \ No newline at end of file diff --git a/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.schema.json b/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.schema.json new file mode 100644 index 00000000..b386105e --- /dev/null +++ b/prez/reference_data/cql/bounded_temporal_interval_relation_matrix.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "A collection of temporal interval relations and their definitions", + "patternProperties": { + "^T_[A-Z_]+$": { + "type": "object", + "description": "A temporal interval relation", + "properties": { + "definition": { + "type": "string", + "format": "uri", + "description": "A URL linking to the formal definition of the temporal relation" + }, + "interval_interval": { + "$ref": "#/$defs/comparisonSet" + }, + "instant_interval": { + "$ref": "#/$defs/comparisonSet" + }, + "interval_instant": { + "$ref": "#/$defs/comparisonSet" + }, + "instant_instant": { + "$ref": "#/$defs/comparisonSet" + } + }, + "required": ["definition", "interval_interval"], + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "temporalOperand": { + "type": "string", + "enum": ["t1_start", "t1_end", "t2_start", "t2_end", "t1_instant", "t2_instant"], + "description": "A temporal operand representing a start, end, or instant of an interval" + }, + "comparisonOperator": { + "type": "string", + "enum": ["<", ">", "="], + "description": "An operator for comparing temporal operands" + }, + "condition": { + "type": "array", + "description": "A single condition in the form [left_operand, operator, right_operand]", + "items": [ + { "$ref": "#/$defs/temporalOperand" }, + { "$ref": "#/$defs/comparisonOperator" }, + { "$ref": "#/$defs/temporalOperand" } + ], + "minItems": 3, + "maxItems": 3 + }, + "conditionSet": { + "type": "object", + "properties": { + "logic": { + "type": "string", + "enum": ["AND", "OR"], + "description": "The logical relationship between the conditions" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/$defs/condition" + }, + "minItems": 1 + } + }, + "required": ["conditions"], + "additionalProperties": false + }, + "comparisonSet": { + "type": "object", + "patternProperties": { + "^[BUI]{1,2}_[BUI]{1,2}$": { + "oneOf": [ + { + "$ref": "#/$defs/conditionSet" + }, + { + "type": "boolean", + "enum": [false], + "description": "Indicates that the relation is impossible for this combination of bounded/unbounded intervals or instants" + } + ] + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/prez/reference_data/cql/default_context.json b/prez/reference_data/cql/default_context.json index 7db23111..c720041f 100644 --- a/prez/reference_data/cql/default_context.json +++ b/prez/reference_data/cql/default_context.json @@ -25,9 +25,12 @@ "@id": "cql:date" }, "datetime": { - "@id": "cql:datetime" + "@id": "cql:datetime" }, "timestamp": { - "@id": "cql:timestamp" + "@id": "cql:timestamp" + }, + "interval": { + "@id": "cql:interval" } } diff --git a/prez/reference_data/cql/geo_function_mapping.py b/prez/reference_data/cql/geo_function_mapping.py index ff991f71..b30650ac 100755 --- a/prez/reference_data/cql/geo_function_mapping.py +++ b/prez/reference_data/cql/geo_function_mapping.py @@ -11,4 +11,4 @@ "s_overlaps": GEOF.sfOverlaps, "s_touches": GEOF.sfTouches, "s_crosses": GEOF.sfCrosses, -} \ No newline at end of file +} diff --git a/prez/services/query_generation/cql.py b/prez/services/query_generation/cql.py index e8e3c0be..67a36068 100755 --- a/prez/services/query_generation/cql.py +++ b/prez/services/query_generation/cql.py @@ -1,5 +1,7 @@ +import json from datetime import datetime from decimal import Decimal +from pathlib import Path from typing import Generator from pyld import jsonld @@ -45,29 +47,50 @@ UnaryExpression, BrackettedExpression, ConditionalOrExpression, + BooleanLiteral, ) +from prez.models.query_params import parse_datetime from prez.reference_data.cql.geo_function_mapping import ( cql_sparql_spatial_mapping, ) CQL = Namespace("http://www.opengis.net/doc/IS/cql2/1.0/") +# SUPPORTED_CQL_TIME_OPERATORS = { +# "t_after", +# "t_before", +# "t_equals", +# "t_disjoint", +# "t_intersects", +# } + + +# all CQL time operators SUPPORTED_CQL_TIME_OPERATORS = { "t_after", "t_before", - "t_equals", + "t_contains", "t_disjoint", + "t_during", + "t_equals", + "t_finishedBy", + "t_finishes", "t_intersects", + "t_meets", + "t_metBy", + "t_overlappedBy", + "t_overlaps", + "t_startedBy", + "t_starts", } +UNBOUNDED = "unbounded" -# all CQL time operators -# {"t_after", "t_before", "t_contains", -# "t_disjoint", "t_during", "t_equals", -# "t_finishedBy", "t_finishes", "t_intersects", -# "t_meets", "t_metBy", "t_overlappedBy", -# "t_overlaps", "t_startedBy", "t_starts"} +relations_path = Path(__file__).parent.parent.parent / ( + "reference_data/cql/bounded_temporal_interval_relation_matrix" ".json" +) +relations = json.loads(relations_path.read_text()) class CQLParser: @@ -267,7 +290,6 @@ def _handle_like(self, args, existing_ggps=None): ) ) ggps.add_pattern(filter_gpnt) - # self._append_graph_pattern(ggps, filter_expr) yield ggps def _handle_spatial(self, operator, args, existing_ggps=None): @@ -345,7 +367,6 @@ def _handle_in(self, args, existing_ggps=None): content=InlineData(data_block=DataBlock(block=ildov)) ) ggps.add_pattern(gpnt) - # self._append_graph_pattern(ggps, gpnt) yield ggps def _extract_spatial_info(self, coordinates_list, args): @@ -364,46 +385,139 @@ def _extract_spatial_info(self, coordinates_list, args): coordinates = format_coordinates_as_wkt(bbox_values, coordinates) return coordinates, geom_type - def _handle_temporal(self, operator, args, existing_ggps=None): + def _handle_temporal(self, comp_func, args, existing_ggps=None): ggps = existing_ggps if existing_ggps is not None else GroupGraphPatternSub() - assert len(args) == 2 - prop_uri = date = None - for arg in args: + if len(args) != 2: + raise ValueError( + f"Temporal operator {comp_func} requires exactly 2 arguments." + ) + + operands = {} + for i, arg in enumerate(args, start=1): + # check if the arg is an interval + interval_list = arg.get(str(CQL.interval)) + if interval_list: + for n, item in enumerate(interval_list): + label = "start" if n == 0 else "end" + prop = item.get(str(CQL.property)) + if prop: + self._triple_for_time_prop(ggps, i, label, prop, operands) + date_val = item.get("@value") + if date_val: + self._dt_to_rdf_literal(i, date_val, label, operands) + continue + + # handle instants - prop and date + label = "instant" + # check if the arg is a property prop = arg.get(str(CQL.property)) if prop: - prop_uri = prop[0].get("@id") + self._triple_for_time_prop(ggps, i, label, prop, operands) + continue + + # check if the arg is a date date = ( arg.get(str(CQL.date)) or arg.get(str(CQL.datetime)) or arg.get(str(CQL.timestamp)) ) if date: - date = date[0].get("@value") - - if operator == "t_before": - gpnt = create_temporal_filter_gpnt(datetime.fromisoformat(date), "<") - elif operator == "t_after": - gpnt = create_temporal_filter_gpnt(datetime.fromisoformat(date), ">") - elif operator == "t_equals": - gpnt = create_temporal_filter_gpnt( - datetime.fromisoformat(date), "=" - ) # potentially could do straight triple pattern matching - elif operator == "t_disjoint": - gpnt = create_temporal_or_gpnt( - datetime.fromisoformat(date), "<", datetime.fromisoformat(date), ">" - ) - elif operator == "t_intersects": - gpnt = create_temporal_or_gpnt( - datetime.fromisoformat(date), ">=", datetime.fromisoformat(date), "<=" - ) + date_val = date[0].get("@value") + self._dt_to_rdf_literal(i, date_val, label, operands) + + gpnt = self.process_temporal_function(comp_func, operands) - self._add_triple( - ggps, Var(value="focus_node"), IRI(value=prop_uri), Var(value="datetime") - ) ggps.add_pattern(gpnt) yield ggps + def get_type_and_bound(self, operands, prefix): + """ + Get the type label and abbreviation for a temporal operand. + Options are "instant" with "I", or "interval" with "U" (unbounded) or "B" (bounded). + """ + if f"{prefix}_instant" in operands: + return "instant", "I" + elif f"{prefix}_start" in operands or f"{prefix}_end" in operands: + start_bound = "U" if operands.get(f"{prefix}_start") is UNBOUNDED else "B" + end_bound = "U" if operands.get(f"{prefix}_end") is UNBOUNDED else "B" + return "interval", start_bound + end_bound + else: + raise ValueError(f"Invalid operand for {prefix}") + + def process_temporal_function(self, comp_func, operands): + t1_type, t1_bound = self.get_type_and_bound(operands, "t1") + t2_type, t2_bound = self.get_type_and_bound(operands, "t2") + + comparison_type = f"{t1_type}_{t2_type}" + + if comparison_type not in relations[comp_func]: + raise ValueError( + f"The {comp_func} function is not applicable to {comparison_type} comparisons." + ) + + key = f"{t1_bound}_{t2_bound}" + + result = relations[comp_func][comparison_type].get(key) + + if result is True or result is False: + return create_filter_bool_gpnt(result) + elif isinstance(result, dict): + negated = relations[comp_func].get("negated", False) + conditions = result["conditions"] + logic = result.get( + "logic", "AND" + ) # Default to AND if logic is not specified + comparisons = [ + (operands[left], op, operands[right]) for left, op, right in conditions + ] + if logic == "AND": + return create_temporal_and_gpnt(comparisons) + elif logic == "OR" and negated: # for t_intersects only + return create_temporal_or_gpnt(comparisons, negated=True) + elif logic == "OR": + return create_temporal_or_gpnt(comparisons) + else: + raise ValueError(f"Unknown logic type: {logic}") + else: + raise ValueError( + f"Unexpected result type for {comp_func} {comparison_type} {key}" + ) + + def _triple_for_time_prop(self, ggps, i, label, prop, operands): + prop_uri = prop[0].get("@id") + value = IRI(value=prop_uri) + var = Var(value=f"dt_{i}_{label}") + operands[f"t{i}_{label}"] = var + self._add_triple(ggps, Var(value="focus_node"), value, var) + + def _handle_interval_list(self, all_args, comparator_args, interval_list): + for item in interval_list: + if item.get(str(CQL.property)): + prop = item.get(str(CQL.property))[0].get("@id") + comparator_args.append(IRI(value=prop)) + elif item.get("@value"): + val = item.get("@value") + # self._dt_to_rdf_literal(comparator_args, val) + dt, _ = parse_datetime(val) + comparator_args.append( + RDFLiteral( + value=dt.isoformat(), + datatype=IRI(value="http://www.w3.org/2001/XMLSchema#dateTime"), + ) + ) + all_args.append(comparator_args) + + def _dt_to_rdf_literal(self, i, dt_str, label, operands): + if dt_str == "..": + operands[f"t{i}_{label}"] = UNBOUNDED + else: + dt, _ = parse_datetime(dt_str) + operands[f"t{i}_{label}"] = RDFLiteral( + value=dt.isoformat(), + datatype=IRI(value="http://www.w3.org/2001/XMLSchema#dateTime"), + ) + def format_coordinates_as_wkt(bbox_values): if len(bbox_values) == 4: @@ -460,10 +574,19 @@ def create_temporal_filter_gpnt(dt: datetime, op: str) -> GraphPatternNotTriples def create_temporal_or_gpnt( - dt1: datetime, op1: str, dt2: datetime, op2: str + comparisons: list[tuple[Var | RDFLiteral, str, Var | RDFLiteral]], + negated=False ) -> GraphPatternNotTriples: + """ + Create a FILTER with multiple conditions joined by OR (||). + + Format: FILTER ( comp1 op1 comp2 || comp3 op2 comp4 || ... ) + + if negated: + Format: FILTER (! (comp1 op1 comp2 || comp3 op2 comp4 || ...) ) + """ _and_expressions = [] - for dt, op in [(dt1, op1), (dt2, op2)]: + for left_comp, op, right_comp in comparisons: if op not in ["=", "<=", ">=", "<", ">"]: raise ValueError(f"Invalid operator: {op}") _and_expressions.append( @@ -476,7 +599,7 @@ def create_temporal_or_gpnt( base_expression=MultiplicativeExpression( base_expression=UnaryExpression( primary_expression=PrimaryExpression( - content=Var(value="datetime") + content=left_comp ) ) ) @@ -488,12 +611,7 @@ def create_temporal_or_gpnt( base_expression=MultiplicativeExpression( base_expression=UnaryExpression( primary_expression=PrimaryExpression( - content=RDFLiteral( - value=dt.isoformat(), - datatype=IRI( - value="http://www.w3.org/2001/XMLSchema#dateTime" - ), - ) + content=right_comp ) ) ) @@ -504,13 +622,142 @@ def create_temporal_or_gpnt( ] ) ) - GraphPatternNotTriples( + if not negated: + return GraphPatternNotTriples( + content=Filter( + constraint=Constraint( + content=BrackettedExpression( + expression=Expression( + conditional_or_expression=ConditionalOrExpression( + conditional_and_expressions=_and_expressions + ) + ) + ) + ) + ) + ) + else: + return GraphPatternNotTriples( + content=Filter( + constraint=Constraint( + content=BrackettedExpression( + expression=Expression( + conditional_or_expression=ConditionalOrExpression( + conditional_and_expressions=[ + ConditionalAndExpression( + value_logicals=[ + ValueLogical( + relational_expression=RelationalExpression( + left=NumericExpression( + additive_expression=AdditiveExpression( + base_expression=MultiplicativeExpression( + base_expression=UnaryExpression( + operator="!", + primary_expression=PrimaryExpression( + content=BrackettedExpression( + expression=Expression( + conditional_or_expression=ConditionalOrExpression( + conditional_and_expressions=_and_expressions + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ] + ) + ] + ) + ) + ) + ) + ) + ) + + +def create_filter_bool_gpnt(boolean: bool) -> GraphPatternNotTriples: + """ + For filtering out all results in scenarios where the input arguments are valid but logically determine that the + filter will filter out all results. + + generates FILTER(false) or FILTER(true) + """ + return GraphPatternNotTriples( + content=Filter( + constraint=Constraint( + content=BrackettedExpression( + expression=Expression.from_primary_expression( + primary_expression=PrimaryExpression( + content=BooleanLiteral(value=boolean) + ) + ) + ) + ) + ) + ) + + +def create_temporal_and_gpnt( + comparisons: list[tuple[Var | RDFLiteral, str, Var | RDFLiteral]] +) -> GraphPatternNotTriples: + """ + Create a FILTER with multiple conditions joined by AND. + + :param comparisons: List of tuples, each containing (left_comp, operator, right_comp) + :return: GraphPatternNotTriples + + Format: + FILTER ( comp1 op1 comp2 && comp3 op2 comp4 && ... ) + """ + _vl_expressions = [] + + for left_comp, op, right_comp in comparisons: + if op not in ["=", "<=", ">=", "<", ">"]: + raise ValueError(f"Invalid operator: {op}") + + _vl_expressions.append( + ValueLogical( + relational_expression=RelationalExpression( + left=NumericExpression( + additive_expression=AdditiveExpression( + base_expression=MultiplicativeExpression( + base_expression=UnaryExpression( + primary_expression=PrimaryExpression( + content=left_comp + ) + ) + ) + ) + ), + operator=op, + right=NumericExpression( + additive_expression=AdditiveExpression( + base_expression=MultiplicativeExpression( + base_expression=UnaryExpression( + primary_expression=PrimaryExpression( + content=right_comp + ) + ) + ) + ) + ), + ) + ) + ) + + return GraphPatternNotTriples( content=Filter( constraint=Constraint( content=BrackettedExpression( expression=Expression( conditional_or_expression=ConditionalOrExpression( - conditional_and_expressions=_and_expressions + conditional_and_expressions=[ + ConditionalAndExpression(value_logicals=_vl_expressions) + ] ) ) ) diff --git a/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq b/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq new file mode 100644 index 00000000..3f61c22d --- /dev/null +++ b/test_data/cql/expected_generated_queries/additional_temporal_disjoint_instant.rq @@ -0,0 +1,7 @@ +CONSTRUCT { +?focus_node ?dt_1_instant +} +WHERE { +?focus_node ?dt_1_instant +FILTER (?dt_1_instant > "2012-08-10T05:30:00+00:00"^^ || ?dt_1_instant < "2012-08-10T05:30:00+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq b/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq new file mode 100644 index 00000000..c3d32c5d --- /dev/null +++ b/test_data/cql/expected_generated_queries/additional_temporal_during_intervals.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start > "2017-06-10T07:30:00+00:00"^^ && ?dt_1_end < "2017-06-11T10:30:00+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/clause7_12.rq b/test_data/cql/expected_generated_queries/clause7_12.rq new file mode 100644 index 00000000..dd4a5abe --- /dev/null +++ b/test_data/cql/expected_generated_queries/clause7_12.rq @@ -0,0 +1,7 @@ +CONSTRUCT { +?focus_node ?dt_1_instant +} +WHERE { +?focus_node ?dt_1_instant +FILTER (! (?dt_1_instant > "1969-07-24T16:50:35+00:00"^^ || ?dt_1_instant < "1969-07-16T05:32:00+00:00"^^)) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/clause7_13.rq b/test_data/cql/expected_generated_queries/clause7_13.rq new file mode 100644 index 00000000..a1ce5696 --- /dev/null +++ b/test_data/cql/expected_generated_queries/clause7_13.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start > "1969-07-16T13:32:00+00:00"^^ && ?dt_1_end < "1969-07-24T16:50:35+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/clause7_17.rq b/test_data/cql/expected_generated_queries/clause7_17.rq new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test_data/cql/expected_generated_queries/clause7_17.rq @@ -0,0 +1 @@ + diff --git a/test_data/cql/expected_generated_queries/example20.rq b/test_data/cql/expected_generated_queries/example20.rq index 039506eb..ce6a0ae3 100644 --- a/test_data/cql/expected_generated_queries/example20.rq +++ b/test_data/cql/expected_generated_queries/example20.rq @@ -1,27 +1,7 @@ CONSTRUCT { -?focus_node . -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 . -?focus_node ?datetime +?focus_node ?dt_1_instant } WHERE { - - -{ -SELECT DISTINCT ?focus_node -WHERE { -?focus_node ?datetime -FILTER (?datetime < "2015-01-01T00:00:00"^^) -}LIMIT 10 -OFFSET 0 -} - -OPTIONAL { -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 - - -} +?focus_node ?dt_1_instant +FILTER (?dt_1_instant < "2015-01-01T00:00:00"^^) } \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example21.rq b/test_data/cql/expected_generated_queries/example21.rq index ed2c9d32..1ef93f98 100644 --- a/test_data/cql/expected_generated_queries/example21.rq +++ b/test_data/cql/expected_generated_queries/example21.rq @@ -1,27 +1,7 @@ CONSTRUCT { -?focus_node . -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 . -?focus_node ?datetime +?focus_node ?dt_1_instant } WHERE { - - -{ -SELECT DISTINCT ?focus_node -WHERE { -?focus_node ?datetime -FILTER (?datetime > "2012-06-05T00:00:00"^^) -}LIMIT 10 -OFFSET 0 -} - -OPTIONAL { -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 - - -} +?focus_node ?dt_1_instant +FILTER (?dt_1_instant > "2012-06-05T00:00:00"^^) } \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example22.rq b/test_data/cql/expected_generated_queries/example22.rq new file mode 100644 index 00000000..c3d32c5d --- /dev/null +++ b/test_data/cql/expected_generated_queries/example22.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start > "2017-06-10T07:30:00+00:00"^^ && ?dt_1_end < "2017-06-11T10:30:00+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example27.rq b/test_data/cql/expected_generated_queries/example27.rq new file mode 100644 index 00000000..373d83d4 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example27.rq @@ -0,0 +1,7 @@ +CONSTRUCT { +?focus_node ?datetime +} +WHERE { +?focus_node ?datetime +FILTER (?datetime > "2012-06-05T00:00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example53.rq b/test_data/cql/expected_generated_queries/example53.rq index e4bcdec5..4f1ae886 100644 --- a/test_data/cql/expected_generated_queries/example53.rq +++ b/test_data/cql/expected_generated_queries/example53.rq @@ -1,27 +1,7 @@ CONSTRUCT { -?focus_node . -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 . -?focus_node ?datetime +?focus_node ?dt_1_instant } WHERE { - - -{ -SELECT DISTINCT ?focus_node -WHERE { -?focus_node ?datetime -FILTER (?datetime > "2010-02-10T00:00:00"^^) -}LIMIT 10 -OFFSET 0 -} - -OPTIONAL { -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 - - -} +?focus_node ?dt_1_instant +FILTER (?dt_1_instant > "2010-02-10T00:00:00"^^) } \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example54.rq b/test_data/cql/expected_generated_queries/example54.rq index 72372e3d..8d1ae435 100644 --- a/test_data/cql/expected_generated_queries/example54.rq +++ b/test_data/cql/expected_generated_queries/example54.rq @@ -1,27 +1,7 @@ CONSTRUCT { -?focus_node . -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 . -?focus_node ?datetime +?focus_node ?dt_1_instant } WHERE { - - -{ -SELECT DISTINCT ?focus_node -WHERE { -?focus_node ?datetime -FILTER (?datetime < "2012-08-10T05:30:00+00:00"^^) -}LIMIT 10 -OFFSET 0 -} - -OPTIONAL { -?focus_node ?prof_1_node_3 . -?prof_1_node_1 ?prof_1_node_2 . -?focus_node ?prof_1_node_1 - - -} +?focus_node ?dt_1_instant +FILTER (?dt_1_instant < "2012-08-10T05:30:00+00:00"^^) } \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example55.rq b/test_data/cql/expected_generated_queries/example55.rq new file mode 100644 index 00000000..1c71d701 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example55.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("2000-01-01T00:00:00+00:00"^^ < ?dt_2_start && "2005-01-10T01:01:01.393216+00:00"^^ > ?dt_2_end) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example56.rq b/test_data/cql/expected_generated_queries/example56.rq new file mode 100644 index 00000000..4cf45fcc --- /dev/null +++ b/test_data/cql/expected_generated_queries/example56.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("2005-01-10T01:01:01.393216+00:00"^^ < ?dt_2_start) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example57.rq b/test_data/cql/expected_generated_queries/example57.rq new file mode 100644 index 00000000..1e25ab9c --- /dev/null +++ b/test_data/cql/expected_generated_queries/example57.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start > "2005-01-10T00:00:00"^^ && ?dt_1_end < "2010-02-10T00:00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example58.rq b/test_data/cql/expected_generated_queries/example58.rq new file mode 100644 index 00000000..f12a8efc --- /dev/null +++ b/test_data/cql/expected_generated_queries/example58.rq @@ -0,0 +1,7 @@ +CONSTRUCT { +?focus_node ?dt_1_instant +} +WHERE { +?focus_node ?dt_1_instant +FILTER (?dt_1_instant = "1851-04-29T00:00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example59.rq b/test_data/cql/expected_generated_queries/example59.rq new file mode 100644 index 00000000..0990ae97 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example59.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start < "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end = "2010-02-10T05:29:20.073225+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example60.rq b/test_data/cql/expected_generated_queries/example60.rq new file mode 100644 index 00000000..e601b608 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example60.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start > "1991-10-07T00:00:00"^^ && ?dt_1_end = "2010-02-10T05:29:20.073225+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example61.rq b/test_data/cql/expected_generated_queries/example61.rq new file mode 100644 index 00000000..e8ff1c78 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example61.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (! (?dt_1_end < "1991-10-07T08:21:06.393262+00:00"^^ || ?dt_1_start > "2010-02-10T05:29:20.073225+00:00"^^)) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example62.rq b/test_data/cql/expected_generated_queries/example62.rq new file mode 100644 index 00000000..f9fb02ce --- /dev/null +++ b/test_data/cql/expected_generated_queries/example62.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("2010-02-10T00:00:00"^^ = ?dt_2_start) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example63.rq b/test_data/cql/expected_generated_queries/example63.rq new file mode 100644 index 00000000..0fd6c765 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example63.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("2010-02-10T05:29:20.073225+00:00"^^ = ?dt_2_end) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example64.rq b/test_data/cql/expected_generated_queries/example64.rq new file mode 100644 index 00000000..d709e1bb --- /dev/null +++ b/test_data/cql/expected_generated_queries/example64.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("1991-10-07T08:21:06.393262+00:00"^^ > ?dt_2_start && "1991-10-07T08:21:06.393262+00:00"^^ < ?dt_2_end && "2010-02-10T05:29:20.073225+00:00"^^ > ?dt_2_end) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example65.rq b/test_data/cql/expected_generated_queries/example65.rq new file mode 100644 index 00000000..5801e893 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example65.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start < "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end > "1991-10-07T08:21:06.393262+00:00"^^ && ?dt_1_end < "1992-10-09T08:08:08.393473+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example66.rq b/test_data/cql/expected_generated_queries/example66.rq new file mode 100644 index 00000000..b5d0c198 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example66.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start +} +WHERE { +?focus_node ?dt_2_end . +?focus_node ?dt_2_start + +FILTER ("1991-10-07T08:21:06.393262+00:00"^^ = ?dt_2_start && "2010-02-10T05:29:20.073225+00:00"^^ > ?dt_2_end) +} \ No newline at end of file diff --git a/test_data/cql/expected_generated_queries/example67.rq b/test_data/cql/expected_generated_queries/example67.rq new file mode 100644 index 00000000..eca86355 --- /dev/null +++ b/test_data/cql/expected_generated_queries/example67.rq @@ -0,0 +1,10 @@ +CONSTRUCT { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start +} +WHERE { +?focus_node ?dt_1_end . +?focus_node ?dt_1_start + +FILTER (?dt_1_start = "1991-10-07T08:21:06.393262+00:00"^^) +} \ No newline at end of file diff --git a/test_data/cql/input/additional_temporal_disjoint_instant.json b/test_data/cql/input/additional_temporal_disjoint_instant.json new file mode 100644 index 00000000..b3e0c8fb --- /dev/null +++ b/test_data/cql/input/additional_temporal_disjoint_instant.json @@ -0,0 +1,7 @@ +{ + "op": "t_disjoint", + "args": [ + { "property": "updated_at" }, + { "timestamp": "2012-08-10T05:30:00Z" } + ] +} diff --git a/test_data/cql/input/additional_temporal_during_intervals.json b/test_data/cql/input/additional_temporal_during_intervals.json new file mode 100644 index 00000000..34033d15 --- /dev/null +++ b/test_data/cql/input/additional_temporal_during_intervals.json @@ -0,0 +1,7 @@ +{ + "op": "t_during", + "args": [ + { "interval": [ { "property": "starts_at" }, { "property": "ends_at" } ] }, + { "interval": [ "2017-06-10T07:30:00Z", "2017-06-11T10:30:00Z" ] } + ] +} diff --git a/test_data/cql/input/additional_temporal_intersects_instant.json b/test_data/cql/input/additional_temporal_intersects_instant.json new file mode 100644 index 00000000..b3e0c8fb --- /dev/null +++ b/test_data/cql/input/additional_temporal_intersects_instant.json @@ -0,0 +1,7 @@ +{ + "op": "t_disjoint", + "args": [ + { "property": "updated_at" }, + { "timestamp": "2012-08-10T05:30:00Z" } + ] +} diff --git a/tests/test_cql_time.py b/tests/test_cql_time.py index e16b3cef..4d40790f 100755 --- a/tests/test_cql_time.py +++ b/tests/test_cql_time.py @@ -3,11 +3,32 @@ import pytest +from prez.services.query_generation.cql import CQLParser + cql_time_filenames = [ - "example20.json", # t_before - "example21.json", # t_after - "example54.json", # t_before - "example53.json", # t_after + "example20.json", # t_before instant + "example21.json", # t_after instant + "example22.json", # t_during + "example53.json", # t_after instant + "example54.json", # t_before instant + "example55.json", # t_contains interval + "example56.json", # t_disjoint interval + "example57.json", # t_during + "clause7_13.json", # t_during + # "clause7_17.json", # t_during + "additional_temporal_disjoint_instant.json", + "example58.json", # t_equals instant + "example59.json", # t_finishedBy interval + "example60.json", # t_finishes interval + "additional_temporal_during_intervals.json", # t_before interval + "example61.json", # t_intersects interval + "example62.json", # t_meets interval + "example63.json", # t_metBy interval + "example64.json", # t_overlappedBy interval + "example65.json", # t_overlaps interval + "example66.json", # t_startedBy interval + "example67.json", # t_starts interval + "clause7_12.json", # t_intersects ] cql_time_generated_queries = [ @@ -19,15 +40,24 @@ "cql_json_filename, output_query_filename", [i for i in (zip(cql_time_filenames, cql_time_generated_queries))], ) -def test_simple_post(client, cql_json_filename, output_query_filename): +def test_time_funcs(cql_json_filename, output_query_filename): cql_json_path = ( Path(__file__).parent.parent / f"test_data/cql/input/{cql_json_filename}" ) cql_json = json.loads(cql_json_path.read_text()) - output_query = ( + reference_query = ( Path(__file__).parent.parent / f"test_data/cql/expected_generated_queries/{output_query_filename}" ).read_text() - headers = {"content-type": "application/json", "accept": "application/sparql-query"} - response = client.post("/cql", json=cql_json, headers=headers) - assert response.text == output_query + context = json.load( + ( + Path(__file__).parent.parent + / "prez/reference_data/cql/default_context.json" + ).open() + ) + cql_parser = CQLParser(cql=cql_json, context=context) + cql_parser.generate_jsonld() + cql_parser.parse() + if not cql_parser.query_str == reference_query: + print(f"\n{cql_parser.query_str}") + assert cql_parser.query_str == reference_query diff --git a/tests/test_geojson_to_wkt.py b/tests/test_geojson_to_wkt.py index e28968c1..0071e8db 100644 --- a/tests/test_geojson_to_wkt.py +++ b/tests/test_geojson_to_wkt.py @@ -3,36 +3,137 @@ from prez.services.query_generation.cql import get_wkt_from_coords -@pytest.mark.parametrize("geom_type, coordinates, expected_wkt, expected_wkt_alternative", [ - ("Point", [0.123, 0.456], "POINT (0.123 0.456)", None), - ("Point", [10.123456, -85.123456], "POINT (10.123456 -85.123456)", None), - ("MultiPoint", [[0.1, 0.1], [1.1, 1.1]], "MULTIPOINT ((0.1 0.1), (1.1 1.1))", "MULTIPOINT (0.1 0.1, 1.1 1.1)"), - ("MultiPoint", [[10.5, 40.5], [40.25, 30.75], [20.123, 20.456], [30.0001, 10.0001]], - "MULTIPOINT ((10.5 40.5), (40.25 30.75), (20.123 20.456), (30.0001 10.0001))", - "MULTIPOINT (10.5 40.5, 40.25 30.75, 20.123 20.456, 30.0001 10.0001)"), - ("LineString", [[0.0, 0.0], [1.5, 1.5], [2.25, 2.25]], "LINESTRING (0.0 0.0, 1.5 1.5, 2.25 2.25)", "LINESTRING (0.00 0.00, 1.50 1.50, 2.25 2.25)"), - ("LineString", [[100.123, 0.123], [101.456, 1.456], [102.789, 2.789]], - "LINESTRING (100.123 0.123, 101.456 1.456, 102.789 2.789)", None), - ("MultiLineString", [[[0.1, 0.1], [1.1, 1.1]], [[2.2, 2.2], [3.3, 3.3]]], - "MULTILINESTRING ((0.1 0.1, 1.1 1.1), (2.2 2.2, 3.3 3.3))", None), - ("MultiLineString", [[[100.001, 0.001], [101.001, 1.001]], [[102.002, 2.002], [103.002, 3.002]]], - "MULTILINESTRING ((100.001 0.001, 101.001 1.001), (102.002 2.002, 103.002 3.002))", None), - ("Polygon", [[[100.01, 0.01], [101.02, 0.01], [101.02, 1.02], [100.01, 1.02], [100.01, 0.01]]], - "POLYGON ((100.01 0.01, 101.02 0.01, 101.02 1.02, 100.01 1.02, 100.01 0.01))", None), - ("Polygon", [[[35.001, 10.001], [45.002, 45.002], [15.003, 40.003], [10.004, 20.004], [35.001, 10.001]], - [[20.005, 30.005], [35.006, 35.006], [30.007, 20.007], [20.005, 30.005]]], - "POLYGON ((35.001 10.001, 45.002 45.002, 15.003 40.003, 10.004 20.004, 35.001 10.001), (20.005 30.005, 35.006 35.006, 30.007 20.007, 20.005 30.005))", None), - ("MultiPolygon", [[[[0.1, 0.1], [1.1, 1.1], [1.1, 0.1], [0.1, 0.1]]], [[[2.2, 2.2], [3.3, 3.3], [3.3, 2.2], [2.2, 2.2]]]], - "MULTIPOLYGON (((0.1 0.1, 1.1 1.1, 1.1 0.1, 0.1 0.1)), ((2.2 2.2, 3.3 3.3, 3.3 2.2, 2.2 2.2)))", None), - ("MultiPolygon", [[[[102.001, 2.001], [103.001, 2.001], [103.001, 3.001], [102.001, 3.001], [102.001, 2.001]]], - [[[100.002, 0.002], [101.002, 0.002], [101.002, 1.002], [100.002, 1.002], [100.002, 0.002]], - [[100.503, 0.503], [100.753, 0.503], [100.753, 0.753], [100.503, 0.753], [100.503, 0.503]]]], - "MULTIPOLYGON (((102.001 2.001, 103.001 2.001, 103.001 3.001, 102.001 3.001, 102.001 2.001)), ((100.002 0.002, 101.002 0.002, 101.002 1.002, 100.002 1.002, 100.002 0.002), (100.503 0.503, 100.753 0.503, 100.753 0.753, 100.503 0.753, 100.503 0.503)))", None), -]) -def test_get_wkt_from_coords_valid(geom_type, coordinates, expected_wkt, expected_wkt_alternative): - assert get_wkt_from_coords(coordinates, geom_type) == expected_wkt or expected_wkt_alternative +@pytest.mark.parametrize( + "geom_type, coordinates, expected_wkt, expected_wkt_alternative", + [ + ("Point", [0.123, 0.456], "POINT (0.123 0.456)", None), + ("Point", [10.123456, -85.123456], "POINT (10.123456 -85.123456)", None), + ( + "MultiPoint", + [[0.1, 0.1], [1.1, 1.1]], + "MULTIPOINT ((0.1 0.1), (1.1 1.1))", + "MULTIPOINT (0.1 0.1, 1.1 1.1)", + ), + ( + "MultiPoint", + [[10.5, 40.5], [40.25, 30.75], [20.123, 20.456], [30.0001, 10.0001]], + "MULTIPOINT ((10.5 40.5), (40.25 30.75), (20.123 20.456), (30.0001 10.0001))", + "MULTIPOINT (10.5 40.5, 40.25 30.75, 20.123 20.456, 30.0001 10.0001)", + ), + ( + "LineString", + [[0.0, 0.0], [1.5, 1.5], [2.25, 2.25]], + "LINESTRING (0.0 0.0, 1.5 1.5, 2.25 2.25)", + "LINESTRING (0.00 0.00, 1.50 1.50, 2.25 2.25)", + ), + ( + "LineString", + [[100.123, 0.123], [101.456, 1.456], [102.789, 2.789]], + "LINESTRING (100.123 0.123, 101.456 1.456, 102.789 2.789)", + None, + ), + ( + "MultiLineString", + [[[0.1, 0.1], [1.1, 1.1]], [[2.2, 2.2], [3.3, 3.3]]], + "MULTILINESTRING ((0.1 0.1, 1.1 1.1), (2.2 2.2, 3.3 3.3))", + None, + ), + ( + "MultiLineString", + [ + [[100.001, 0.001], [101.001, 1.001]], + [[102.002, 2.002], [103.002, 3.002]], + ], + "MULTILINESTRING ((100.001 0.001, 101.001 1.001), (102.002 2.002, 103.002 3.002))", + None, + ), + ( + "Polygon", + [ + [ + [100.01, 0.01], + [101.02, 0.01], + [101.02, 1.02], + [100.01, 1.02], + [100.01, 0.01], + ] + ], + "POLYGON ((100.01 0.01, 101.02 0.01, 101.02 1.02, 100.01 1.02, 100.01 0.01))", + None, + ), + ( + "Polygon", + [ + [ + [35.001, 10.001], + [45.002, 45.002], + [15.003, 40.003], + [10.004, 20.004], + [35.001, 10.001], + ], + [ + [20.005, 30.005], + [35.006, 35.006], + [30.007, 20.007], + [20.005, 30.005], + ], + ], + "POLYGON ((35.001 10.001, 45.002 45.002, 15.003 40.003, 10.004 20.004, 35.001 10.001), (20.005 30.005, 35.006 35.006, 30.007 20.007, 20.005 30.005))", + None, + ), + ( + "MultiPolygon", + [ + [[[0.1, 0.1], [1.1, 1.1], [1.1, 0.1], [0.1, 0.1]]], + [[[2.2, 2.2], [3.3, 3.3], [3.3, 2.2], [2.2, 2.2]]], + ], + "MULTIPOLYGON (((0.1 0.1, 1.1 1.1, 1.1 0.1, 0.1 0.1)), ((2.2 2.2, 3.3 3.3, 3.3 2.2, 2.2 2.2)))", + None, + ), + ( + "MultiPolygon", + [ + [ + [ + [102.001, 2.001], + [103.001, 2.001], + [103.001, 3.001], + [102.001, 3.001], + [102.001, 2.001], + ] + ], + [ + [ + [100.002, 0.002], + [101.002, 0.002], + [101.002, 1.002], + [100.002, 1.002], + [100.002, 0.002], + ], + [ + [100.503, 0.503], + [100.753, 0.503], + [100.753, 0.753], + [100.503, 0.753], + [100.503, 0.503], + ], + ], + ], + "MULTIPOLYGON (((102.001 2.001, 103.001 2.001, 103.001 3.001, 102.001 3.001, 102.001 2.001)), ((100.002 0.002, 101.002 0.002, 101.002 1.002, 100.002 1.002, 100.002 0.002), (100.503 0.503, 100.753 0.503, 100.753 0.753, 100.503 0.753, 100.503 0.503)))", + None, + ), + ], +) +def test_get_wkt_from_coords_valid( + geom_type, coordinates, expected_wkt, expected_wkt_alternative +): + assert ( + get_wkt_from_coords(coordinates, geom_type) == expected_wkt + or expected_wkt_alternative + ) + # Shapely appears to have a bug with input Polygon formats. The above tests fails ONLY for Polygon for Shapely. # Geomet works with: # [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]] -# which appears to be as per spec. \ No newline at end of file +# which appears to be as per spec.