From 0e000204ad0611649a22061f9c2e4ebf1770d417 Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 3 Sep 2021 12:23:13 +0200 Subject: [PATCH 1/3] Add RequestHandlerDelegate.contentTypes to return the list of content types this handler can serve, default to JSON. QgsServerOgcApiHandler::contentTypes not available in Python bindings --- wmtsCacheServer/apiutils.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/wmtsCacheServer/apiutils.py b/wmtsCacheServer/apiutils.py index be950f6..9a8d721 100644 --- a/wmtsCacheServer/apiutils.py +++ b/wmtsCacheServer/apiutils.py @@ -64,7 +64,7 @@ def prepare(self, **values): pass def href(self, path: str="", extension: str="") -> str: - """ Returns an URL to self, to be used for links to the current resources + """ Returns an URL to self, to be used for links to the current resources and as a base for constructing links to sub-resources """ return self._parent.href(self._context,path,extension) @@ -113,10 +113,10 @@ def send_error(self, status_code: int = 500, **kwargs: Any) -> None: self.write(dict(status="error" if status_code != 200 else "ok", httpcode = status_code, error = { "message": self._reason })) - + if status_code > 300: QgsMessageLog.logMessage(f"Returned HTTP Error {status_code}: {self._reason}" ,"wmtsCacheApi",Qgis.Critical) - + if not self._finished: self.finish() @@ -181,7 +181,7 @@ class RequestHandlerDelegate(QgsServerOgcApiHandler): """ # XXX We need to preserve instances from garbage - # collection + # collection __instances = [] def __init__(self, path: str, handler: Type[RequestHandler], @@ -189,8 +189,10 @@ def __init__(self, path: str, handler: Type[RequestHandler], kwargs: Dict={}): super().__init__() + self._content_types = [] if content_types: self.setContentTypes(content_types) + self._content_types = content_types self._path = QRegularExpression(path) self._name = handler.__name__ self._handler = handler @@ -198,6 +200,11 @@ def __init__(self, path: str, handler: Type[RequestHandler], self.__instances.append(self) + def contentTypes(self): + """ QgsServerOgcApiHandler::contentTypes not available in Python bindings + """ + return self._content_types + def path(self): return self._path @@ -224,7 +231,7 @@ def parameters(self, context): return [] def handleRequest(self, context): - """ + """ """ handler = self._handler(self,context) handler.initialize(**self._kwargs) From 33d1ae12751f8626ebf2e1809d0ba426fa779eac Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 3 Sep 2021 14:55:51 +0200 Subject: [PATCH 2/3] Add RequestHandler.links to get the self and alternate links for the given request The QgsServerOgcApiHandler::links not available in Python bindings --- wmtsCacheServer/apiutils.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/wmtsCacheServer/apiutils.py b/wmtsCacheServer/apiutils.py index 9a8d721..a021c31 100644 --- a/wmtsCacheServer/apiutils.py +++ b/wmtsCacheServer/apiutils.py @@ -67,7 +67,22 @@ def href(self, path: str="", extension: str="") -> str: """ Returns an URL to self, to be used for links to the current resources and as a base for constructing links to sub-resources """ - return self._parent.href(self._context,path,extension) + return self._parent.href(self._context, path, extension) + + def links(self) -> List[str]: + """Returns the self and alternate links for the given request + QgsServerOgcApiHandler::links not available in Python bindings + """ + links = list() + current_ct = self._parent.contentTypeFromRequest(self._request) + for ct in self._parent.contentTypes(): + links.append({ + 'href': self.href('', QgsServerOgcApi.contentTypeToExtension(ct) if ct != QgsServerOgcApi.JSON else ''), + 'rel': QgsServerOgcApi.relToString(QgsServerOgcApi.self if ct == current_ct else QgsServerOgcApi.alternate), + "type": QgsServerOgcApi.mimeType(ct), + "title": self._parent.linkTitle()+' as '+QgsServerOgcApi.contentTypeToString(ct), + }) + return links def finish(self, chunk: Optional[Union[str, bytes, dict]] = None) -> None: """ Terminate the request From 4f466fc66665cbeb236cb4171c5a12712aff9187 Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 3 Sep 2021 14:57:20 +0200 Subject: [PATCH 3/3] Use RequestHandler.links method in cache manager API --- tests/test_wmts_cachemngrapi.py | 40 ++++++++++----- wmtsCacheServer/cachemngrapi.py | 90 ++++++++++++++++++++------------- 2 files changed, 83 insertions(+), 47 deletions(-) diff --git a/tests/test_wmts_cachemngrapi.py b/tests/test_wmts_cachemngrapi.py index ea51f50..2c54233 100644 --- a/tests/test_wmts_cachemngrapi.py +++ b/tests/test_wmts_cachemngrapi.py @@ -33,8 +33,11 @@ def test_wmts_cachemngrapi_empty_cache(client): json_content = json.loads(rv.content) assert 'links' in json_content - assert len(json_content['links']) == 1 - assert json_content['links'][0]['title'] == 'Cache collections' + assert len(json_content['links']) == 2 + assert json_content['links'][0]['title'] == 'WMTS Cache manager LandingPage as JSON' + assert json_content['links'][0]['rel'] == 'self' + assert json_content['links'][1]['title'] == 'WMTS Cache manager Collections as JSON' + assert json_content['links'][1]['rel'] == 'data' qs = "/wmtscache/collections" rv = client.get(qs) @@ -47,6 +50,9 @@ def test_wmts_cachemngrapi_empty_cache(client): assert 'collections' in json_content assert len(json_content['collections']) == 0 assert 'links' in json_content + assert len(json_content['links']) == 1 + assert json_content['links'][0]['title'] == 'WMTS Cache manager Collections as JSON' + assert json_content['links'][0]['rel'] == 'self' def test_wmts_cachemngrapi_cache_info(client): @@ -112,7 +118,7 @@ def test_wmts_cachemngrapi_cache_info(client): "TILEMATRIX": "0", "TILEROW": "0", "TILECOL": "0", - "FORMAT": "image/png" + "FORMAT": "image/png" } # Get the cached path from the request parameters @@ -148,7 +154,9 @@ def test_wmts_cachemngrapi_cache_info(client): assert len(json_content['collections']) == 1 assert 'links' in json_content - assert len(json_content['links']) == 0 + assert len(json_content['links']) == 1 + assert json_content['links'][0]['title'] == 'WMTS Cache manager Collections as JSON' + assert json_content['links'][0]['rel'] == 'self' collection = json_content['collections'][0] assert 'id' in collection @@ -169,7 +177,9 @@ def test_wmts_cachemngrapi_cache_info(client): assert json_content['project'] == client.getprojectpath("france_parts.qgs").strpath assert 'links' in json_content - assert len(json_content['links']) == 2 + assert len(json_content['links']) == 3 + assert json_content['links'][0]['title'] == 'WMTS Cache manager ProjectCollection as JSON' + assert json_content['links'][0]['rel'] == 'self' qs = "/wmtscache/collections/{}/docs".format(collection['id']) rv = client.get(qs) @@ -187,7 +197,9 @@ def test_wmts_cachemngrapi_cache_info(client): assert json_content['documents'] == 1 assert 'links' in json_content - assert len(json_content['links']) == 0 + assert len(json_content['links']) == 1 + assert json_content['links'][0]['title'] == 'WMTS Cache manager DocumentCollection as JSON' + assert json_content['links'][0]['rel'] == 'self' qs = "/wmtscache/collections/{}/layers".format(collection['id']) rv = client.get(qs) @@ -205,7 +217,9 @@ def test_wmts_cachemngrapi_cache_info(client): assert len(json_content['layers']) == 1 assert 'links' in json_content - assert len(json_content['links']) == 0 + assert len(json_content['links']) == 1 + assert json_content['links'][0]['title'] == 'WMTS Cache manager LayerCollection as JSON' + assert json_content['links'][0]['rel'] == 'self' layer = json_content['layers'][0] assert 'id' in layer @@ -221,7 +235,9 @@ def test_wmts_cachemngrapi_cache_info(client): assert json_content['id'] == layer['id'] assert 'links' in json_content - assert len(json_content['links']) == 0 + assert len(json_content['links']) == 1 + assert json_content['links'][0]['title'] == 'WMTS Cache manager LayerCache as JSON' + assert json_content['links'][0]['rel'] == 'self' def test_wmts_cachemngrapi_delete_docs(client): @@ -356,7 +372,7 @@ def test_wmts_cachemngrapi_delete_layer(client): "TILEMATRIX": "0", "TILEROW": "0", "TILECOL": "0", - "FORMAT": "image/png" + "FORMAT": "image/png" } # Get the cached path from the request parameters @@ -478,7 +494,7 @@ def test_wmts_cachemngrapi_delete_layers(client): "TILEMATRIX": "0", "TILEROW": "0", "TILECOL": "0", - "FORMAT": "image/png" + "FORMAT": "image/png" } # Get the cached path from the request parameters @@ -610,7 +626,7 @@ def test_wmts_cachemngrapi_delete_collection(client): "TILEMATRIX": "0", "TILEROW": "0", "TILECOL": "0", - "FORMAT": "image/png" + "FORMAT": "image/png" } # Get the cached path from the request parameters @@ -746,7 +762,7 @@ def test_wmts_cachemngrapi_layerid_error(client): "TILEMATRIX": "0", "TILEROW": "0", "TILECOL": "0", - "FORMAT": "image/png" + "FORMAT": "image/png" } # Get the cached path from the request parameters diff --git a/wmtsCacheServer/cachemngrapi.py b/wmtsCacheServer/cachemngrapi.py index 71b2f62..303dbb6 100644 --- a/wmtsCacheServer/cachemngrapi.py +++ b/wmtsCacheServer/cachemngrapi.py @@ -32,7 +32,7 @@ def collect(): project = inf.read_text() # (name, project) yield (name,project) - + return (metadata, collect()) @@ -58,13 +58,23 @@ class LandingPage(RequestHandler): """ Project collections listing handler """ def get(self) -> None: + + def extra_links(): + """ Build links to collections + """ + for ct in self._parent.contentTypes(): + if ct != QgsServerOgcApi.JSON: + # Collections only implement JSON + continue + yield { + "href": self.href("/collections", QgsServerOgcApi.contentTypeToExtension(ct) if ct != QgsServerOgcApi.JSON else ''), + "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.data), + "type": QgsServerOgcApi.mimeType(ct), + "title": 'WMTS Cache manager Collections as '+QgsServerOgcApi.contentTypeToString(ct), + } + data = { - 'links': [{ - "href": self.href("/collections"), - "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.data), - "type": QgsServerOgcApi.mimeType(QgsServerOgcApi.JSON), - "title": "Cache collections", - }] + 'links': self.links() + list(extra_links()) } self.write(data) @@ -77,21 +87,31 @@ def get(self) -> None: """ metadata, coll = read_metadata_collection(self.rootdir) - def links(): + def collection_links(name): + """ Build links to collection + """ + for ct in self._parent.contentTypes(): + if ct != QgsServerOgcApi.JSON: + # ProjectCollection only implement JSON + continue + yield { + "href": self.href(f"/{name}", QgsServerOgcApi.contentTypeToExtension(ct) if ct != QgsServerOgcApi.JSON else ''), + "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.item), + "type": QgsServerOgcApi.mimeType(ct), + "title": 'WMTS Cache manager ProjectCollection as '+QgsServerOgcApi.contentTypeToString(ct), + } + + def collections(): for name,project in coll: yield { 'id': name, 'project': project, - 'links': [{ - "href": self.href(f"/{name}"), - "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.item), - "type": QgsServerOgcApi.mimeType(QgsServerOgcApi.JSON), - "title": "Cache collection", - }]} + 'links': list(collection_links(name)) + } data = { "cache_layout": metadata['layout'], - "collections": list(links()), - "links": [], # self.links(context) + "collections": list(collections()), + "links": self.links(), } self.write(data) @@ -100,7 +120,7 @@ def links(): class MetadataMixIn: def get_metadata(self, collectionid: str): - """ Return project metadata + """ Return project metadata """ try: project, layers = read_project_metadata(self.rootdir, collectionid) @@ -121,14 +141,14 @@ class ProjectCollection(RequestHandler,MetadataMixIn): """ def get(self, collectionid: str): - """ Return project metadata + """ Return project metadata """ metadata, project, layers = self.get_metadata(collectionid) - def links(): + def layer_collections(): for layer in layers: yield { 'id': layer, - 'links': [{ + 'links': [{ 'href': self.href(f"/layers/{layer}"), 'rel': QgsServerOgcApi.relToString(QgsServerOgcApi.item), 'type': QgsServerOgcApi.mimeType(QgsServerOgcApi.JSON), @@ -138,8 +158,8 @@ def links(): data = { 'id': collectionid, 'project': project, - 'layers' : list(links()), - 'links' : [ + 'layers' : list(layer_collections()), + 'links' : self.links() + [ { "href": self.href("/docs"), "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.item), @@ -151,9 +171,9 @@ def links(): "rel": QgsServerOgcApi.relToString(QgsServerOgcApi.item), "type": QgsServerOgcApi.mimeType(QgsServerOgcApi.JSON), "title": "Cache collection layers", - }, + }, ], - } + } self.write(data) @@ -178,7 +198,7 @@ def delete(self, collectionid) -> None: class DocumentCollection(RequestHandler,MetadataMixIn): - """ Return documentation about project + """ Return documentation about project """ def get(self, collectionid): @@ -192,7 +212,7 @@ def get(self, collectionid): 'id': collectionid, 'project': project, 'documents': sum( 1 for _ in docroot.glob('*.xml')), - 'links': [], # self.links(context) + 'links': self.links(), } self.write(data) @@ -210,7 +230,7 @@ def delete(self, collectionid ) -> None: class LayerCollection(RequestHandler,MetadataMixIn): - """ + """ """ def get(self, collectionid): @@ -218,7 +238,7 @@ def get(self, collectionid): """ metadata,project,layers = self.get_metadata(collectionid) - def links(): + def layer_collections(): for layer in layers: yield { 'id': layer, 'links': [{ @@ -231,8 +251,8 @@ def links(): data = { 'id': collectionid, 'project': project, - 'layers' : list(links()), - 'links' : [], # self.links(context) + 'layers' : list(layer_collections()), + 'links' : self.links(), } self.write(data) @@ -254,19 +274,19 @@ class LayerCache(RequestHandler,MetadataMixIn): """ def get( self, collectionid: str, layerid: str) -> None: - """ + """ """ metadata, project, layers = self.get_metadata(collectionid) if layerid not in layers: raise HTTPError(404,reason=f"Layer '{layerid}' not found") - + data = { 'id': layerid, - 'links':[], + 'links':self.links(), } self.write(data) - + def delete( self, collectionid: str, layerid: str) -> None: """ List projects """ @@ -274,7 +294,7 @@ def delete( self, collectionid: str, layerid: str) -> None: if layerid not in layers: raise HTTPError(404,reason=f"Layer '{layerid}' not found") - cache = self.cache_helper(metadata) + cache = self.cache_helper(metadata) cache = CacheHelper(self.rootdir, metadata['layout']) # Remove tiles