diff --git a/src/extensions.py b/src/extensions.py index e521179e54cdeda2415b6e9c3405738e674b405b..d2218cf744304ac94c992bd07bdfb954a6142f17 100644 --- a/src/extensions.py +++ b/src/extensions.py @@ -33,6 +33,7 @@ import sys import os.path import logging + class ExtensionManager(object): def __init__(self, ): @@ -62,7 +63,7 @@ class ExtensionManager(object): f(*args, **kwargs) def register(self, name, f=None): - if f == None: + if f is None: def decorator(f): self.register(name, f) return f @@ -70,4 +71,5 @@ class ExtensionManager(object): self.extentions.setdefault(name, []).append(f) + plugins = ExtensionManager() diff --git a/src/metadata.py b/src/metadata.py index f5dc65f23983d88b66a668974a16e0722a9cd2d4..1e7142a79ecf91c3a1551a8939ee1b2969d1cb3a 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -36,8 +36,10 @@ import yaml import contextlib from mapscript import MapServerError + METADATA_NAME="mra" + def get_metadata(obj, key, *args): """Returns Metadata for a mapObj or a layerObj. get_metadata(obj, key, [default]) -> value @@ -52,7 +54,7 @@ def get_metadata(obj, key, *args): except MapServerError: value = None - if value == None: + if value is None: if not args: raise KeyError(key) value = args[0] @@ -62,6 +64,7 @@ def get_metadata(obj, key, *args): return value + def iter_metadata_keys(obj): # Prepare for ugliness... @@ -73,24 +76,30 @@ def iter_metadata_keys(obj): return keys + def get_metadata_keys(obj): return list(iter_metadata_keys(obj)) + def set_metadata(obj, key, value): obj.setMetaData(key, value) + def set_metadatas(obj, metadatas): # TODO: erease all metadata first. for key, value in metadatas.iteritems(): set_metadata(obj, key, value) + def update_metadatas(obj, metadatas): for key, value in metadatas.iteritems(): set_metadata(obj, key, value) + def del_metadata(obj, key): obj.removeMetaData(key) + @contextlib.contextmanager def metadata(obj, key, *args): """Context manager that exposes the metadata and saves it again. @@ -101,6 +110,7 @@ def metadata(obj, key, *args): yield metadata set_metadata(obj, key, metadata) + def __get_mra(obj): text = get_metadata(obj, METADATA_NAME, None) if text is None: @@ -112,9 +122,11 @@ def __get_mra(obj): raise IOError("File has corrupted MRA metadata for entry \"%s\"." % key) return metadata + def __save_mra(obj, mra_metadata): set_metadata(obj, METADATA_NAME, yaml.safe_dump(mra_metadata)) + def get_mra_metadata(obj, key, *args): """get_metadata(obj, key, [default]) -> value""" @@ -130,27 +142,34 @@ def get_mra_metadata(obj, key, *args): raise return args[0] + def iter_mra_metadata_keys(obj): return __get_mra(obj).iterkeys() + def get_mra_metadata_keys(obj): return __get_mra(obj).keys() + def update_mra_metadatas(obj, update): with mra_metadata(obj) as metadata: metadata.update(update) + def set_mra_metadatas(obj, mra_metadata): __save_mra(obj, mra_metadata) + def set_mra_metadata(obj, key, value): with mra_metadata(obj) as metadata: metadata[key] = value + def del_mra_metadata(obj, key, value): with mra_metadata(obj) as mra_metadata: del metadata[key] + @contextlib.contextmanager def mra_metadata(obj, *args): """Context manager that exposes the mra_metadata and saves it again. diff --git a/src/mra.py b/src/mra.py index 9e381d544b066381f25c13498f588c31d7ca8711..8fc2c17e54d7fb56056c95d292817d9b86d32322 100644 --- a/src/mra.py +++ b/src/mra.py @@ -51,6 +51,7 @@ from webapp import KeyExists import stores import metadata + class MetadataMixin(object): def __getattr__(self, attr): if hasattr(self, "ms") and hasattr(metadata, attr): @@ -58,6 +59,7 @@ class MetadataMixin(object): raise AttributeError("\"%s\" object has no attribute \"%s\"." % (type(self).__name__, attr)) + class Clazz(object): def __init__(self, backend): self.ms = backend @@ -65,13 +67,14 @@ class Clazz(object): def index(self): return self.index + class Layer(MetadataMixin): def __init__(self, backend): self.ms = backend def update(self, name, enabled, metadata): self.ms.name = name - self.ms.template = "foo.html" # TODO: Support html format response + self.ms.template = "foo.html" # TODO: Support html format response self.enable(enabled) self.update_metadatas(metadata) @@ -214,6 +217,7 @@ class Layer(MetadataMixin): if c.ms.group == s_name: self.ms.removeClass(c.index) + class LayerGroup(object): def __init__(self, name, mapfile): @@ -270,6 +274,7 @@ class LayerGroup(object): return extent + class Mapfile(MetadataMixin): def __init__(self, path, create=False, needed=False): @@ -289,7 +294,7 @@ class Mapfile(MetadataMixin): # and adding some default values... self.ms.name = self.name self.ms.setProjection("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs") - self.ms.setExtent(-180,-90,180,90) + self.ms.setExtent(-180, -90, 180, 90) self.ms.units = mapscript.MS_DD self.set_metadata("ows_title", "OGC Web Service") self.set_metadata("ows_srs", "EPSG:4326") @@ -412,15 +417,18 @@ class Mapfile(MetadataMixin): with self.mra_metadata("layergroups", {}) as layergroups: del layergroups[lg_name] + # Workspaces are special Mapfiles that are composed of LayerModels # which are layers that can be used to configure other layers. + class LayerModel(Layer): def __init__(self, ws, *args, **kwargs): Layer.__init__(self, *args, **kwargs) self.ws = ws self.name = self.get_mra_metadata("name", None) + class FeatureTypeModel(LayerModel): def update(self, ds_name, ft_name, metadata): ws = self.ws @@ -490,11 +498,11 @@ class FeatureTypeModel(LayerModel): layer.ms.connection = self.ms.connection layer.update_mra_metadatas({ - "name": self.get_mra_metadata("name"), - "type": self.get_mra_metadata("type"), - "storage": self.get_mra_metadata("storage"), - "workspace": ws.name, - }) + "name": self.get_mra_metadata("name"), + "type": self.get_mra_metadata("type"), + "storage": self.get_mra_metadata("storage"), + "workspace": ws.name, + }) # if enabled: # layer.set_metadata("wfs_enable_request", "GetCapabilities DescribeFeatureType GetFeature") @@ -514,7 +522,7 @@ class FeatureTypeModel(LayerModel): field_names.append(field.get_name()) geometry_column = ft.get_geometry_column() - if geometry_column == None: + if geometry_column is None: geometry_column = "geometry" layer.set_metadatas({ "ows_include_items": ",".join(field_names), @@ -526,7 +534,7 @@ class FeatureTypeModel(LayerModel): "wfs_getfeature_formatlist": "OGRGML,SHAPEZIP", }) - if ft.get_fid_column() != None: + if ft.get_fid_column() is not None: layer.set_metadatas({ "wfs_featureid": ft.get_fid_column(), "gml_featureid": ft.get_fid_column(), @@ -534,6 +542,7 @@ class FeatureTypeModel(LayerModel): plugins.extend("post_configure_vector_layer", self, ws, ds, ft, layer) + class CoverageModel(LayerModel): def update(self, cs_name, c_name, metadata): @@ -586,29 +595,30 @@ class CoverageModel(LayerModel): layer.ms.setProjection(self.ms.getProjection()) layer.ms.setExtent(self.ms.extent.minx, self.ms.extent.miny, self.ms.extent.maxx, self.ms.extent.maxy) - layer.ms.setProcessingKey("RESAMPLE","AVERAGE") + layer.ms.setProcessingKey("RESAMPLE", "AVERAGE") layer.ms.data = self.ms.data layer.ms.connectiontype = self.ms.connectiontype layer.ms.connection = self.ms.connection layer.update_mra_metadatas({ - "name": self.get_mra_metadata("name"), - "type": self.get_mra_metadata("type"), - "storage": self.get_mra_metadata("storage"), - "workspace": ws.name, - }) + "name": self.get_mra_metadata("name"), + "type": self.get_mra_metadata("type"), + "storage": self.get_mra_metadata("storage"), + "workspace": ws.name, + }) layer.set_metadatas({ - "wcs_name": layer.get_metadata("ows_name"), - "wcs_label": layer.get_metadata("ows_title"), - "wcs_description": layer.get_metadata("ows_abstract") - }) + "wcs_name": layer.get_metadata("ows_name"), + "wcs_label": layer.get_metadata("ows_title"), + "wcs_description": layer.get_metadata("ows_abstract") + }) # if enabled: # layer.set_metadata("wcs_enable_request", "GetCapabilities DescribeCoverage GetCoverage") plugins.extend("post_configure_raster_layer", self, ws, layer) + class Workspace(Mapfile): def __init__(self, mra, *args, **kwargs): @@ -706,7 +716,7 @@ class Workspace(Mapfile): try: next(self.iter_featuretypemodels(name)) except StopIteration: - pass # No layers use our store, all OK. + pass # No layers use our store, all OK. else: raise ValueError("The datastore \"%s\" can't be delete because it is used." % name) return self.delete_store("datastore", name) @@ -749,7 +759,7 @@ class Workspace(Mapfile): try: next(self.iter_coveragemodels(name)) except StopIteration: - pass # No layers use our store, all OK. + pass # No layers use our store, all OK. else: raise ValueError("The coveragestore \"%s\" can't be delete because it is used." % name) return self.delete_store("coveragestore", name) @@ -776,7 +786,7 @@ class Workspace(Mapfile): def iter_layermodels(self, st_type=None, store=None, **kwargs): if st_type: kwargs.setdefault("mra", {})["type"] = st_type - if store != None: + if store is not None: kwargs.setdefault("mra", {})["storage"] = store for ms_layer in self.iter_ms_layers(**kwargs): yield self.__ms2model(ms_layer) @@ -853,8 +863,10 @@ class Workspace(Mapfile): def delete_coveragemodel(self, cs_name, c_name): return self.delete_layermodel("coverage", cs_name, c_name) + # Finaly the global context: + class MRA(object): def __init__(self, config_path): try: @@ -891,8 +903,8 @@ class MRA(object): # Fonts: def get_fontset_path(self, *args): - root = self.config["storage"].get("fontset", - "/".join([self.get_resource_path("fonts"), "fonts.txt"])) + root = self.config["storage"].get( + "fontset", "/".join([self.get_resource_path("fonts"), "fonts.txt"])) return self.get_resource_path(root, *args) def list_fontset(self): diff --git a/src/mralogs.py b/src/mralogs.py index d1cc8cbf309c374c7eebb96f7c4027bdeaa5d570..4a9a1a2ebd12e356a894f1bfa0eb9fe2450f93de 100644 --- a/src/mralogs.py +++ b/src/mralogs.py @@ -35,6 +35,7 @@ import inspect import logging import functools + def setup(log_level, log_file=None, format="%(asctime)s %(levelname)7s: %(message)s"): log_level = getattr(logging, log_level.upper()) @@ -46,6 +47,7 @@ def setup(log_level, log_file=None, format="%(asctime)s %(levelname)7s: %(messag sh.setFormatter(logging.Formatter(format)) logging.getLogger().addHandler(sh) + class Reccord(logging.Handler): """A logging.Handler class which stores the records. You'll probably want to iterate on this class to get the results. @@ -83,6 +85,7 @@ class Reccord(logging.Handler): logger.removeHandler(logging.getLogger(self.logger)) logging.Handler.__del__(self) + def short_str(obj, length=15, delta=5, tail="..."): """Returns a short version of str(obj) of length +/- delta. It tries to cut on characters from string.punctuation. @@ -120,6 +123,7 @@ def logIn(level="debug", filter=(lambda *a, **kw:True)): return decorator(f) return decorator + def logOut(level="debug", filter=(lambda *a, **kw:True)): """Decorator factory used to log when the function returns. The log level can be specified using level. @@ -144,6 +148,7 @@ def logOut(level="debug", filter=(lambda *a, **kw:True)): return decorator(f) return decorator + def logBoth(level="debug"): """Helper decorator, same as applying both @logIn and @logOut""" def decorator(f): diff --git a/src/pyhtml.py b/src/pyhtml.py index cc1b36e997d30b23f70ff1a3df884d970c70f1bf..9dec3b660a2e007994dced7ffec5dba5eef8247b 100644 --- a/src/pyhtml.py +++ b/src/pyhtml.py @@ -36,6 +36,7 @@ from xml.etree import ElementTree as etree from cgi import escape from xml.sax.saxutils import unescape + def should_be_url(s): """This is used to find out if it might be a good idea to consider a string is a URL. @@ -44,7 +45,10 @@ def should_be_url(s): parsed = urlparse.urlparse(s) return parsed.scheme and parsed.netloc + __dump_xml_max_element_id = 0 + + def dump_xml(xml, fp, indent=0, indent_depth=2, reinit=True): """Recursive function that transforms an ElementTree to html written to the file like stream fp. @@ -101,11 +105,12 @@ def dump(obj, fp, indent=0, *args, **kwargs): """Writes the html represention of obj to the file-like object fp. This uses pyxml to first transform the object into xml. *args and **kwargs are forwarded to pyxml.xml() - + """ xml = pyxml.xml(obj, *args, **kwargs) dump_xml(xml, fp, indent) + def dumps(obj, *args, **kwargs): """Returns the html representation of obj as a string.""" stream = StringIO.StringIO() diff --git a/src/pyxml.py b/src/pyxml.py index c48ceb38f671778a0bdde5032e653fc9d44f2374..a8550a9c62cf7100802f8ddf69f4079b195f4731 100644 --- a/src/pyxml.py +++ b/src/pyxml.py @@ -33,6 +33,7 @@ import xml.etree.ElementTree as etree from xml.etree.ElementTree import Element from xml.sax.saxutils import escape + # Here we have the Entries, it wraps either a list or a dict. # Its not trivial because we want to inherit from a list or # from a dict, but we want both instances to inherit from Entries. @@ -65,19 +66,20 @@ class Entries(object): def href(link, element=None): """Builds an atom:link object for the link.""" - if element == None: + if element is None: element = Element("atom:link") element.tag = "atom:link" element.attrib = { - "xmlns:atom":"http://www.w3.org/2005/Atom", - "rel":"alternate", - "href":link, - "type":"application/xml", - } + "xmlns:atom": "http://www.w3.org/2005/Atom", + "rel": "alternate", + "href": link, + "type": "application/xml", + } return element + def singular(name): """Tries to return name in its singular form if possible else it just returns name.""" if name.endswith("ies"): @@ -86,6 +88,7 @@ def singular(name): return name[:-1] return name + def default_xml_dict_mapper(obj_name, key_name="key"): """Maps to xml_dict and tries to deduce the entry naming from obj_name. If obj_name is plural then entries's tag will the singular and a key_name @@ -98,6 +101,7 @@ def default_xml_dict_mapper(obj_name, key_name="key"): else: return xml_dict, None + def default_xml_list_mapper(obj_name, entry_name="entry"): """Always maps to xml_list but tries to name its entries after the singular of obj_name if possible. If not they are named after entry_name. @@ -109,6 +113,7 @@ def default_xml_list_mapper(obj_name, entry_name="entry"): else: return xml_list, entry_name + def default_xml_mapper(obj, obj_name, dict_mapper=default_xml_dict_mapper, list_mapper=default_xml_list_mapper): @@ -125,7 +130,7 @@ def default_xml_mapper(obj, obj_name, and otherwise it is assumed to be a string. """ - if obj == None: + if obj is None: return None, None elif isinstance(obj, Entries): return obj.get_hints() @@ -146,6 +151,7 @@ def default_xml_mapper(obj, obj_name, else: raise NotImplementedError("Can't map %s object." % type(obj)) + def xml(obj, obj_name=None, parent=None, xml_mapper=default_xml_mapper, dict_mapper=default_xml_dict_mapper, @@ -157,11 +163,11 @@ def xml(obj, obj_name=None, parent=None, """ # Findout the object's name. - if obj_name == None: - obj_name = parent.tag if parent != None else "object" + if obj_name is None: + obj_name = parent.tag if parent is not None else "object" # Create the parent if it's not provided. - if parent == None: + if parent is None: parent = etree.Element(tag=obj_name) mapper, hint = xml_mapper(obj, obj_name, dict_mapper, list_mapper) @@ -172,16 +178,19 @@ def xml(obj, obj_name=None, parent=None, return parent + def xml_href(parent, obj, hint=None, xml_mapper=default_xml_mapper, dict_mapper=default_xml_dict_mapper, list_mapper=default_xml_list_mapper): """Adds obj to parent as if it is a href.""" href(str(obj), parent) + def xml_string(parent, obj, _=None, xml_mapper=default_xml_mapper, dict_mapper=default_xml_dict_mapper, list_mapper=default_xml_list_mapper): """Adds obj to parent as if it is a string.""" parent.text = escape(str(obj)) + def xml_dict(parent, obj, hint=None, xml_mapper=default_xml_mapper, dict_mapper=default_xml_dict_mapper, list_mapper=default_xml_list_mapper): """Adds obj to parent as if it is a dictionary. @@ -197,6 +206,7 @@ def xml_dict(parent, obj, hint=None, xml_mapper=default_xml_mapper, xml(v, parent=child, xml_mapper=xml_mapper, dict_mapper=dict_mapper, list_mapper=list_mapper) parent.append(child) + def xml_list(parent, obj, hint, xml_mapper=default_xml_mapper, dict_mapper=default_xml_dict_mapper, list_mapper=default_xml_list_mapper): """Adds obj to parent as if it is a list. @@ -209,14 +219,17 @@ def xml_list(parent, obj, hint, xml_mapper=default_xml_mapper, xml(v, parent=child, xml_mapper=xml_mapper, dict_mapper=dict_mapper, list_mapper=list_mapper) parent.append(child) + def dump(obj, fp, encoding=None, *args, **kwargs): """Writes the xml represention of obj to the file-like object fp.""" fp.write(etree.tostring(xml(obj, *args, **kwargs), encoding)) + def dumps(obj, encoding=None, *args, **kwargs): """Returns the xml representation of obj as a string.""" return etree.tostring(xml(obj, *args, **kwargs), encoding) + def obj(xml): """Returns the object represented by the xml. Basicaly this is done recursivly in four checks: @@ -275,6 +288,7 @@ def obj(xml): else: return list(obj(c) for c in children) + def loads(s, retname=False, *args, **kwargs): """Returns an object coresponding to what is described in the xml.""" try: @@ -285,10 +299,11 @@ def loads(s, retname=False, *args, **kwargs): o = obj(xml, *args, **kwargs) return (o, xml.tag) if retname else o + def load(fp, retname=False, *args, **kwargs): """Returns an object coresponding to what is described in the xml read from the file-like object fp. - + """ try: xml = etree.parse(fp) @@ -297,4 +312,3 @@ def load(fp, retname=False, *args, **kwargs): raise ValueError("No XML object could be decoded.") o = obj(xml, *args, **kwargs) return (o, xml.tag) if retname else o - diff --git a/src/server.py b/src/server.py index 489467cdad60aea6a7e4d48809d9a8a139c8e87c..e643ff0b899819d4a3f2e369d792efd2d4cce5fe 100755 --- a/src/server.py +++ b/src/server.py @@ -46,13 +46,18 @@ from pyxml import Entries from extensions import plugins import mapscript + # Some helper functions first. + + def get_workspace(ws_name): with webapp.mightNotFound(): return mra.get_workspace(ws_name) + # Now the main classes that handle the REST API. + class index(object): """Index of the API (e.g. http://hostname/mra/) @@ -71,6 +76,7 @@ class index(object): "fonts": href("fonts"), } + class version(object): """To know about used versions... @@ -83,11 +89,12 @@ class version(object): ]} } + class workspaces(object): """Workspaces container. http://hostname/mra/workspaces - + See 'workspace' class documentation for definition of a 'workspace'. """ @@ -113,18 +120,19 @@ class workspaces(object): webapp.Created("%s/workspaces/%s.%s" % (web.ctx.home, ws_name, format)) + class workspace(object): """A workspace is a grouping of data stores and coverage stores. http://hostname/mra/workspaces/<ws> - - In fact, a workspace is assimilated to one mapfile (<workspace_name>.ws.map) - which contains some unactivated layers. These layers allows to configure - connections to data (data store or coverage store) then data + + In fact, a workspace is assimilated to one mapfile (<workspace_name>.ws.map) + which contains some unactivated layers. These layers allows to configure + connections to data (data store or coverage store) then data itself (featuretype for vector type or coverage for raster type). - These layers should not be published as OGC service as such. - However, in the near future (TODO), it should be possible to publish + These layers should not be published as OGC service as such. + However, in the near future (TODO), it should be possible to publish a workspace as permitted GeoServer. And this workspace should be identified as a usual MapFile. @@ -146,6 +154,7 @@ class workspace(object): # TODO: def PUT(... # TODO: def DELETE(... + class datastores(object): """Data stores container. @@ -172,7 +181,7 @@ class datastores(object): ws = get_workspace(ws_name) - data = get_data(name="dataStore", mandatory=["name"], + data = get_data(name="dataStore", mandatory=["name"], authorized=["name", "title", "abstract", "connectionParameters"]) ds_name = data.pop("name") @@ -182,7 +191,8 @@ class datastores(object): ws.save() webapp.Created("%s/workspaces/%s/datastores/%s.%s" % ( - web.ctx.home, ws_name, ds_name, format)) + web.ctx.home, ws_name, ds_name, format)) + class datastore(object): """A data store is a source of spatial data that is vector based. @@ -205,8 +215,8 @@ class datastore(object): return {"dataStore": { "name": info["name"], - "enabled": True, # Always enabled - # TODO: Handle enabled/disabled states + "enabled": True, # Always enabled + # TODO: Handle enabled/disabled states "workspace": { "name": ws.name, "href": "%s/workspaces/%s.%s" % ( @@ -248,6 +258,7 @@ class datastore(object): ws.delete_datastore(ds_name) ws.save() + class featuretypes(object): """Feature types container. @@ -294,11 +305,12 @@ class featuretypes(object): webapp.Created("%s/workspaces/%s/datastores/%s/featuretypes/%s.%s" % ( web.ctx.home, ws.name, ds_name, data["name"], format)) + class featuretype(object): """A feature type is a data set that originates from a data store. http://hostname/mra/workspaces/<ws>/datastores/<ds>/featuretypes/<ft> - + A feature type is considered as a layer under MapServer which is still unactivated. """ @@ -320,7 +332,7 @@ class featuretype(object): latlon_extent = ft.get_latlon_extent() - # About attributs, we apply just values handled by + # About attributs, we apply just values handled by # MapServer in a GetFeature response... attributes = [{ "name": f.get_name(), @@ -335,7 +347,7 @@ class featuretype(object): "name": dsft.get_geometry_column(), "type": dsft.get_geomtype_gml(), "minOccurs": 0, - "maxOccurs": 1, + "maxOccurs": 1, # "nillable": True, # binding? }) @@ -378,10 +390,10 @@ class featuretype(object): # In MRA, it is easier (or more logical?) to keep native CRS, # Or there is a problem of understanding on our part. # So, i prefer to comment 'srs' entry cause we force the - # value of 'projectionPolicy' to 'NONE'... but it is something + # value of 'projectionPolicy' to 'NONE'... but it is something # we should investigate... - "enabled": True, # Always enabled => TODO - "store": { # TODO: add key: class="dataStore" + "enabled": True, # Always enabled => TODO + "store": { # TODO: add key: class="dataStore" "name": ds_name, "href": "%s/workspaces/%s/datastores/%s.%s" % ( web.ctx.home, ws_name, ds_name, format) @@ -421,6 +433,7 @@ class featuretype(object): ws.delete_featuretypemodel(ds_name, ft_name) ws.save() + class coveragestores(object): """Coverage stores container. @@ -443,7 +456,7 @@ class coveragestores(object): @HTTPCompatible() def POST(self, ws_name, format): - """Create new coverage store.""" + """Create new coverage store.""" ws = get_workspace(ws_name) data = get_data(name="coverageStore", mandatory=["name"], authorized=["name", "title", "abstract", "connectionParameters"]) @@ -463,6 +476,7 @@ class coveragestores(object): webapp.Created("%s/workspaces/%s/coveragestores/%s.%s" % ( web.ctx.home, ws_name, cs_name, format)) + class coveragestore(object): """A coverage store is a source of spatial data that is raster based. @@ -503,10 +517,10 @@ class coveragestore(object): @HTTPCompatible() def PUT(self, ws_name, cs_name, format): """Modify coverage store <ds>.""" - + ws = get_workspace(ws_name) - data = get_data(name="coverageStore", - mandatory=["name"], + data = get_data(name="coverageStore", + mandatory=["name"], authorized=["name", "title", "abstract", "connectionParameters"]) if cs_name != data.pop("name"): raise webapp.Forbidden("Can't change the name of a coverage store.") @@ -528,6 +542,7 @@ class coveragestore(object): ws.delete_coveragestore(cs_name) ws.save() + class coverages(object): """Coverages container. @@ -569,6 +584,7 @@ class coverages(object): webapp.Created("%s/workspaces/%s/coveragestores/%s/coverages/%s.%s" % ( web.ctx.home, ws.name, cs_name, data["name"], format)) + class coverage(object): """A coverage is a raster based data set that originates from a coverage store. @@ -596,14 +612,14 @@ class coverage(object): "title": c.get_mra_metadata("title", c.name), "abstract": c.get_mra_metadata("abstract", None), # TODO: Keywords - "nativeCRS": c.get_wkt(), # TODO: Add key class="projected" if projected... + "nativeCRS": c.get_wkt(), # TODO: Add key class="projected" if projected... "srs": "%s:%s" % (c.get_authority_name(), c.get_authority_code()), "nativeBoundingBox": { "minx": extent.minX(), "miny": extent.minY(), "maxx": extent.maxX(), "maxy": extent.maxY(), - "crs": "%s:%s" % (c.get_authority_name(), c.get_authority_code()), # TODO: Add key class="projected" if projected... + "crs": "%s:%s" % (c.get_authority_name(), c.get_authority_code()), # TODO: Add key class="projected" if projected... }, "latLonBoundingBox":{ "minx": latlon_extent.minX(), @@ -612,8 +628,8 @@ class coverage(object): "maxy": latlon_extent.maxY(), "crs": "EPSG:4326" }, - "enabled": True, # Always enabled => TODO - "store": { # TODO: Add attr class="coverageStore" + "enabled": True, # Always enabled => TODO + "store": { # TODO: Add attr class="coverageStore" "name": cs_name, "href": "%s/workspaces/%s/coveragestores/%s.%s" % ( web.ctx.home, ws_name, cs_name, format) @@ -649,7 +665,7 @@ class coverage(object): @HTTPCompatible() def PUT(self, ws_name, cs_name, c_name, format): """Modify coverage <c>.""" - + ws = get_workspace(ws_name) data = get_data(name="coverage", mandatory=["name"], authorized=["name", "title", "abstract"]) @@ -676,6 +692,7 @@ class coverage(object): ws.delete_coveragemodel(c_name, cs_name) ws.save() + class files(object): """ http://hostname/mra/workspaces/<ws>/datastores/<cs>/file.<extension> @@ -710,7 +727,7 @@ class files(object): except KeyError: # Create the store if it seems legit and it does not exist. if st_type == "datastores" or st_type == "coveragestores": - st_type = st_type[:-1] # Remove trailing 's' + st_type = st_type[:-1] # Remove trailing 's' with webapp.mightConflict("Workspace", workspace=ws_name): ws.create_store(st_type, st_name, {}) # Finaly check if its OK now. @@ -737,7 +754,8 @@ class files(object): z.extract(f, path=dest) # Set new connection parameters: - ws.update_store(st_type, st_name, {"connectionParameters":{"url":"file:"+mra.pub_file_path(path)}}) + ws.update_store(st_type, st_name, { + "connectionParameters": {"url": "file:" + mra.pub_file_path(path)}}) ws.save() # Finally we might have to configure it. @@ -751,6 +769,7 @@ class files(object): else: raise webapp.BadRequest(message="configure must be one of 'first', 'none' or 'all'.") + class fonts(object): """Configure available fonts. @@ -761,8 +780,7 @@ class fonts(object): def GET(self, format): """Returns the list of available fonts.""" - return {"fonts": [f_name - for f_name in mra.list_fontset()]} + return {"fonts": [f_name for f_name in mra.list_fontset()]} @HTTPCompatible() def PUT(self, format): @@ -778,14 +796,15 @@ class fonts(object): mra.update_fontset() - # Then updates (the) mapfile(s) only + # Then updates (the) mapfile(s) only # if the fontset path is not specified. # Should it be done here ?... mf = mra.get_available() - if mf.ms.fontset.filename == None: + if mf.ms.fontset.filename is None: mf.ms.setFontSet(mra.get_fontset_path()) mf.save() + class styles(object): """SLD styles container. @@ -804,13 +823,13 @@ class styles(object): @HTTPCompatible() def POST(self, format): - """Create a new SLD style. Add the 'name' parameter in order to specify + """Create a new SLD style. Add the 'name' parameter in order to specify the name to be given to the new style. """ params = web.input(name=None) name = params.name - if name == None: + if name is None: raise webapp.BadRequest(message="no parameter \"name\" given.") data = web.data() @@ -818,12 +837,13 @@ class styles(object): webapp.Created("%s/styles/%s.%s" % (web.ctx.home, name, format)) + class style(object): - """A style describes how a resource (a feature type or a coverage) should be + """A style describes how a resource (a feature type or a coverage) should be symbolized or rendered by a Web Map Service. http://hostname/mra/styles/<s> - + Styles are specified with SLD and translated into the mapfile (with CLASS and STYLE blocs) to be applied. An extension may be considered to manage all style possibilities offered by MapServer. @@ -859,6 +879,7 @@ class style(object): # TODO: def DELETE(... + class layers(object): """Layers container. @@ -923,6 +944,7 @@ class layers(object): webapp.Created("%s/layers/%s.%s" % (web.ctx.home, l_name, format)) + class layer(object): """A layer is a published resource (feature type or coverage) from a MapFile. @@ -949,32 +971,36 @@ class layer(object): "title": layer.get_metadata("ows_title", None), "abstract": layer.get_metadata("ows_abstract", None), "type": layer.get_type_name(), - "resource": { + "resource": { # TODO: Add attr class="featureType|coverage" "name": layer.get_mra_metadata("name"), "href": "%s/workspaces/%s/%ss/%s/%ss/%s.%s" % ( web.ctx.home, layer.get_mra_metadata("workspace"), - store_type, layer.get_mra_metadata("storage"), + store_type, layer.get_mra_metadata("storage"), data_type, layer.get_mra_metadata("name"), format), }, - "enabled": bool(layer.ms.status), # TODO because it's fictitious... + "enabled": bool(layer.ms.status), # TODO because it's fictitious... # "attribution" => TODO? # "path" => TODO? }} # Check CLASSGROUP dflt_style = layer.ms.classgroup - if dflt_style == None: + if dflt_style is None: # If is 'None': take the first style as would MapServer. for s_name in layer.iter_styles(): dflt_style = s_name break - if dflt_style == None: + if dflt_style is None: return response - - response["layer"].update({"defaultStyle": {"name": dflt_style, - "href": "%s/styles/%s.%s" % (web.ctx.home, dflt_style, format)},}) - + + response["layer"].update({ + "defaultStyle": { + "name": dflt_style, + "href": "%s/styles/%s.%s" % (web.ctx.home, dflt_style, format) + } + }) + styles = [{"name": s_name, "href": "%s/styles/%s.%s" % (web.ctx.home, s_name, format), } for s_name in layer.iter_styles() if s_name != dflt_style] @@ -994,7 +1020,7 @@ class layer(object): if l_name != data.get("name", l_name): raise webapp.Forbidden("Can't change the name of a layer.") - l_enabled = True # TODO: => data.pop("enabled", True) + l_enabled = True # TODO: => data.pop("enabled", True) mf = mra.get_available() with webapp.mightNotFound(): @@ -1008,7 +1034,7 @@ class layer(object): except ValueError: raise webapp.NotFound(message="resource \"%s\" was not found." % href) - st_type, r_type = st_type[:-1], r_type[:-1] # Remove trailing s. + st_type, r_type = st_type[:-1], r_type[:-1] # Remove trailing s. ws = get_workspace(ws_name) with webapp.mightNotFound(r_type, workspace=ws_name): @@ -1028,7 +1054,7 @@ class layer(object): with webapp.mightNotFound(): style = mra.get_style(s_name) layer.add_style_sld(mf, s_name, style) - + # Remove the automatic default style. for s_name in layer.iter_styles(): if s_name == tools.get_dflt_sld_name(layer.ms.type): @@ -1045,6 +1071,7 @@ class layer(object): mf.delete_layer(l_name) mf.save() + class layerstyles(object): """Styles container associated to one layer. @@ -1068,6 +1095,7 @@ class layerstyles(object): } for s_name in layer.iter_styles()], } + class layerstyle(object): """Style associated to layer <l>. @@ -1085,6 +1113,7 @@ class layerstyle(object): layer.remove_style(s_name) mf.save() + class layerfields(object): """Attributes (Fields) container associated to layer <l>. @@ -1112,7 +1141,7 @@ class layerfields(object): min_occurs, max_occurs = int(occurs[0]), int(occurs[1]) else: geom, type, min_occurs, max_occurs = "undefined", "undefined", 0, 1 - + fields.append({ "name": geom, "type": type, @@ -1122,6 +1151,7 @@ class layerfields(object): return {"fields": fields} + class layergroups(object): """Layergroups container. @@ -1159,8 +1189,9 @@ class layergroups(object): webapp.Created("%s/layergroups/%s.%s" % (web.ctx.home, lg.name, format)) + class layergroup(object): - """A layergroup is a grouping of layers and styles that can be accessed + """A layergroup is a grouping of layers and styles that can be accessed as a single layer in a WMS GetMap request. http://hostname/mra/layergroups/<lg> @@ -1176,7 +1207,8 @@ class layergroup(object): latlon_extent = lg.get_latlon_extent() - bounds = {"minx": latlon_extent.minX(), + bounds = { + "minx": latlon_extent.minX(), "miny": latlon_extent.minY(), "maxx": latlon_extent.maxX(), "maxy": latlon_extent.maxY(), @@ -1227,6 +1259,7 @@ class layergroup(object): mf.delete_layergroup(lg_name) mf.save() + class OWSGlobalSettings(object): """Control settings of the main OWS service, i.e. the mapfile: layers.map @@ -1255,13 +1288,14 @@ class OWSGlobalSettings(object): data = get_data(name=ows, mandatory=["enabled"], authorized=["enabled"]) is_enabled = data.pop("enabled") # TODO: That would be cool to be able to control each operation... - values = {True: "*", "True": "*", "true": "*", + values = {True: "*", "True": "*", "true": "*", False: "", "False": "", "false": ""} if is_enabled not in values: raise KeyError("\"%s\" is not valid" % is_enabled) mf.set_metadata("%s_enable_request" % ows, values[is_enabled]) mf.save() + # Index: urlmap(index, "") # About version: @@ -1319,4 +1353,4 @@ app = web.application(urls, globals()) if __name__ == "__main__": app.run() -application = app.wsgifunc() \ No newline at end of file +application = app.wsgifunc() diff --git a/src/stores.py b/src/stores.py index 627debf2a864960c79cf2b7b006113a4fa44b765..5c9dc8ac2e172b51c20ee457e1d87224b6593574 100644 --- a/src/stores.py +++ b/src/stores.py @@ -34,6 +34,7 @@ from osgeo import ogr, osr, gdal import mapscript import tools + class Extent(list): def __init__(self, *args, **kwargs): @@ -63,6 +64,7 @@ class Extent(list): self[1] = min(self[1], y) self[3] = max(self[3], y) + class Field(object): """A Field implementation backed by ogr.""" @@ -120,6 +122,7 @@ class Field(object): else: return None + class Feature(object): """A Feature implementation backed by ogr.""" @@ -144,6 +147,7 @@ class Feature(object): def get_field(self): return Field(self.backend.GetFieldDefn(), layer) + class Featuretype(object): """A featuretype implementation backed by ogr.""" @@ -299,6 +303,7 @@ class Featuretype(object): name, nullable = feature.GetField(0), feature.GetField(1) self.nullables[name] = nullable + class Datastore(object): """A datastore implementation backed by ogr.""" @@ -345,6 +350,7 @@ class Datastore(object): for i in xrange(self.backend.GetLayerCount()): yield Featuretype(self.backend.GetLayerByIndex(i), self) + class Band(object): """A band immplementation backed by gdal.""" @@ -353,15 +359,16 @@ class Band(object): self.backend = backend + class Coveragestore(object): """A coveragestore implementation backed by gdal.""" def __init__(self, path): """Path will be used to open the store, it can be a simple filesystem path or something more complex used by gdal/ogr to access databases for example. - + The first argument to __init__ can also directly be a gdal/ogr object. - + """ self.backend = path if isinstance(path, gdal.Dataset) else gdal.Open(path) if self.backend == None: diff --git a/src/tools.py b/src/tools.py index efb65362d40840cc4866fe90dc257058431a02fd..a02459c1499b9acfaf321217ed62753640fc064f 100644 --- a/src/tools.py +++ b/src/tools.py @@ -45,10 +45,12 @@ def ms_version(): """Return the current MapServer version used by MRA""" return mapscript.MS_VERSION + def gdal_version(): """Return the current GDAL version used by MRA""" return gdal.VersionInfo("RELEASE_NAME") + def assert_is_empty(generator, tname, iname): try: next(generator) @@ -57,20 +59,24 @@ def assert_is_empty(generator, tname, iname): else: raise webapp.Forbidden(message="Can't remove \"%s\" because it is an non-empty %s." % (iname, tname)) + def href(url): return pyxml.Entries({"href": url}) + def safe_path_join(root, *args): full_path = os.path.realpath(os.path.join(root, *args)) if not full_path.startswith(os.path.realpath(root)): raise webapp.Forbidden(message="Path \"%s\" outside root directory." % (args)) return full_path + def is_hidden(path): # TODO Add a lot of checks, recursive option (to check folders) # MacOSX has at least four ways to hide files... return os.path.basename(path).startswith(".") + def get_dflt_sld_name(type): # TODO: Names should be changed... if type == 0: # point @@ -82,6 +88,7 @@ def get_dflt_sld_name(type): else: return None + def wkt_to_proj4(wkt): """Return Proj4 definition from WKT definition.""" @@ -89,6 +96,7 @@ def wkt_to_proj4(wkt): srs.ImportFromWkt(wkt) return srs.ExportToProj4() + def proj4_to_wkt(proj4): """Return WKT definition from Proj4 definition.""" @@ -96,6 +104,7 @@ def proj4_to_wkt(proj4): srs.ImportFromProj4(proj4) return srs.ExportToWkt() + def wkt_to_authority(wkt): """Return authority name and authority code from WKT definition.""" @@ -103,9 +112,9 @@ def wkt_to_authority(wkt): srs.ImportFromWkt(wkt) # Are there really no other way with osgeo? - if srs.GetAuthorityCode("PROJCS") != None: + if srs.GetAuthorityCode("PROJCS") is not None: return srs.GetAuthorityName("PROJCS"), srs.GetAuthorityCode("PROJCS") - elif srs.GetAuthorityCode("GEOGCS") != None : + elif srs.GetAuthorityCode("GEOGCS") is not None: return srs.GetAuthorityName("GEOGCS"), srs.GetAuthorityCode("GEOGCS") else: - return "Unknown", "Unknown" # :s it could be improved... (TODO) + return "Unknown", "Unknown" # :s it could be improved... (TODO) diff --git a/src/webapp.py b/src/webapp.py index ffc3318676ad9e1a3d66bd3de89532997746a4d3..8193b4506268305c8c41f2435f0988a60afdcfd6 100644 --- a/src/webapp.py +++ b/src/webapp.py @@ -39,16 +39,20 @@ import os.path import itertools import mralogs + class KeyExists(KeyError): pass + # web.py doesn't allow to set a custom message for all errors, as it does # for NotFound, we attempt to fix that here, but only handle those we use... + def Created(location): web.ctx.status = "201 Created" web.header("Location", location) + class BadRequest(web.webapi.HTTPError): """`400 Bad Request` error.""" def __init__(self, message="bad request"): @@ -57,6 +61,7 @@ class BadRequest(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class NotFound(web.webapi.HTTPError): """`404 Not Found` error.""" def __init__(self, message="not found"): @@ -65,6 +70,7 @@ class NotFound(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class Unauthorized(web.webapi.HTTPError): """`401 Unauthorized` error.""" def __init__(self, message="unauthorized"): @@ -73,6 +79,7 @@ class Unauthorized(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class Forbidden(web.webapi.HTTPError): """`403 Forbidden` error.""" def __init__(self, message="forbidden"): @@ -81,6 +88,7 @@ class Forbidden(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class Conflict(web.webapi.HTTPError): """`409 Conflict` error.""" def __init__(self, message="conflict"): @@ -89,6 +97,7 @@ class Conflict(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class NotAcceptable(web.webapi.HTTPError): """`406 Not Acceptable` error.""" def __init__(self, message="not acceptable"): @@ -97,6 +106,7 @@ class NotAcceptable(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + class NotImplemented(web.webapi.HTTPError): """`501 Not Implemented` error.""" def __init__(self, message="not implemented"): @@ -105,8 +115,10 @@ class NotImplemented(web.webapi.HTTPError): headers = {"Content-Type": "text/html"} web.webapi.HTTPError.__init__(self, status, headers, message) + # The folowing helpers are for managing exceptions and transforming them into http errors: + class exceptionManager(object): raise_all = False @@ -120,6 +132,7 @@ class exceptionManager(object): if not self.raise_all and exc_type in self.exceptions: return not self.handle(exc_type, exc_value, traceback) + class exceptionsToHTTPError(exceptionManager): def __init__(self, message=None, exceptions=None, **kwargs): if message != None: @@ -131,6 +144,7 @@ class exceptionsToHTTPError(exceptionManager): msg = self.message.format(exception=getattr(exc_value, "message", str(exc_value)), **self.msg_args) raise self.HTTPError(message=msg) + class nargString(list): """This object only implements format, which it redirects to one of the strings given to its __init__ according to how many @@ -145,6 +159,7 @@ class nargString(list): raise TypeError("To many arguments for string formatting.") return self[len(args) + len(kwargs)].format(*args, **kwargs) + class mightFailLookup(exceptionsToHTTPError): def __init__(self, name=None, message=None, exceptions=None, **kwargs): if len(kwargs) == 1: @@ -153,6 +168,7 @@ class mightFailLookup(exceptionsToHTTPError): kwargs["name"] = name exceptionsToHTTPError.__init__(self, message, exceptions, **kwargs) + class mightNotFound(mightFailLookup): exceptions = (KeyError, IndexError) HTTPError = NotFound @@ -162,6 +178,7 @@ class mightNotFound(mightFailLookup): "'{exception}' not found in {container_type} {container}.", "{name} '{exception!s}' not found in {container_type} '{container}'.") + class mightConflict(mightFailLookup): exceptions = (KeyExists,) HTTPError = Conflict @@ -171,6 +188,7 @@ class mightConflict(mightFailLookup): "'{exception}' already exists in {container_type} {container}.", "{name} '{exception}' already exists in {container_type} '{container}'.") + class URLMap(object): """Helper class to build url maps for web.py. @@ -257,9 +275,11 @@ class URLMap(object): self.map = [] return iter(map) + # Available for use if you don't want your own instance. urlmap = URLMap() + def default_renderer(format, authorized, content, name_hint): if format == "xml": return pyxml.dumps(content, obj_name=name_hint) @@ -274,10 +294,11 @@ def default_renderer(format, authorized, content, name_hint): render = web.template.render(templates) return render.response(web.ctx.home, web.ctx.path.split("/"), urls, pyhtml.dumps(content, obj_name=name_hint, indent=4)) elif format == "json": - return json.dumps({name_hint:content}) + return json.dumps({name_hint: content}) else: return str(content) + class HTTPCompatible(object): """Decorator factory used to transform the output of a backend function to be suited for the web. @@ -290,10 +311,10 @@ class HTTPCompatible(object): return_logs = False known_mimes = { - "xml" : "application/xml", - "sld" : "application/vnd.ogc.sld+xml", - "html" : "text/html", - "json" : "application/json", + "xml": "application/xml", + "sld": "application/vnd.ogc.sld+xml", + "html": "text/html", + "json": "application/json", } def __init__(self, authorize=set(), forbid=set(), authorized=set(["xml", "json", "html"]), @@ -323,7 +344,7 @@ class HTTPCompatible(object): self.default = default self.renderer = renderer - self.name_hint=name_hint + self.name_hint = name_hint self.parse_format = parse_format if not isinstance(authorize, set): @@ -347,7 +368,7 @@ class HTTPCompatible(object): for the web. """ - if self.render == None: + if self.render is None: # We must guess if we want to render or not. self.render = f.__name__ in ["GET"] @@ -369,13 +390,12 @@ class HTTPCompatible(object): else: page_format = self.default - # TODO: look at web.ctx.env.get("Accept") and take it into account. # Send a NotAcceptable error if we can't agree with the client. # Trim trailing Nones. if self.trim_nones: - while args and args[-1] == None: + while args and args[-1] is None: del args[-1] # Check format against authorized. @@ -408,27 +428,27 @@ class HTTPCompatible(object): name_hint = self.name_hint - if name_hint == None and isinstance(content, dict) and len(content) == 1: + if name_hint is None and isinstance(content, dict) and len(content) == 1: name_hint = next(content.iterkeys()) content = next(content.itervalues()) - elif name_hint == None: + elif name_hint is None: name_hint = "response" # We want to make sure we don't end up doing str(None) - if content == None: + if content is None: content = "" # Lets add the logs to the content. if add_debug: - msgs = [{"asctime":msg.asctime, - "filename":msg.filename, - "funcName":msg.funcName, - "lineno":msg.lineno, - "levelname":msg.levelname, - "message":msg.message, + msgs = [{"asctime": msg.asctime, + "filename": msg.filename, + "funcName": msg.funcName, + "lineno": msg.lineno, + "levelname": msg.levelname, + "message": msg.message, } for msg in reccord] name_hint = "debug_data" - content = {self.name_hint:content, "_logs":msgs} + content = {self.name_hint: content, "_logs": msgs} if self.render and self.renderer: result = self.renderer(page_format, self.authorized, content, name_hint) @@ -442,19 +462,20 @@ class HTTPCompatible(object): self.original_function = f return wrapper + def get_data(name=None, mandatory=[], authorized=[], forbidden=[]): data = web.data() if not data: raise web.badrequest("You must suply some data. (mandatory: %s, authorized: %s)" % (mandatory, authorized)) - if not "CONTENT_TYPE" in web.ctx.env: + if "CONTENT_TYPE" not in web.ctx.env: raise web.badrequest("You must specify a Content-Type.") ctype = web.ctx.env.get("CONTENT_TYPE") try: - if "text/xml" in ctype or "application/xml" in ctype: + if "text/xml" in ctype or "application/xml" in ctype: data, dname = pyxml.loads(data, retname=True) print "received \"%s\"" % dname print data @@ -467,7 +488,7 @@ def get_data(name=None, mandatory=[], authorized=[], forbidden=[]): except (AttributeError, ValueError): raise web.badrequest("Could not decode input data (%s)." % data) - if name and data == None: + if name and data is None: raise web.badrequest("The object you are sending does not contain a \"%s\" entry." % name) if not all(x in data for x in mandatory):