Natureshadow /
OSMAlchemy
| 1 | # ~*~ coding: utf-8 ~*~ |
||
| 2 | #- |
||
| 3 | # OSMAlchemy - OpenStreetMap to SQLAlchemy bridge |
||
| 4 | # Copyright (c) 2016 Dominik George <[email protected]> |
||
| 5 | # |
||
| 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy |
||
| 7 | # of this software and associated documentation files (the "Software"), to deal |
||
| 8 | # in the Software without restriction, including without limitation the rights |
||
| 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
| 10 | # copies of the Software, and to permit persons to whom the Software is |
||
| 11 | # furnished to do so, subject to the following conditions: |
||
| 12 | # |
||
| 13 | # The above copyright notice and this permission notice shall be included in all |
||
| 14 | # copies or substantial portions of the Software. |
||
| 15 | # |
||
| 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
| 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
| 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||
| 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
| 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||
| 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||
| 22 | # SOFTWARE. |
||
| 23 | # |
||
| 24 | # Alternatively, you are free to use OSMAlchemy under Simplified BSD, The |
||
| 25 | # MirOS Licence, GPL-2+, LGPL-2.1+, AGPL-3+ or the same terms as Python |
||
| 26 | # itself. |
||
| 27 | |||
| 28 | 1 | """ Module that holds the main OSMAlchemy class. |
|
| 29 | |||
| 30 | The classe encapsulates the model and accompanying logic. |
||
| 31 | """ |
||
| 32 | |||
| 33 | 1 | from sqlalchemy.engine import Engine |
|
| 34 | 1 | from sqlalchemy.ext.declarative import declarative_base |
|
| 35 | 1 | from sqlalchemy.orm import sessionmaker, scoped_session |
|
| 36 | 1 | try: |
|
| 37 | 1 | from flask_sqlalchemy import SQLAlchemy as FlaskSQLAlchemy |
|
| 38 | except ImportError: |
||
| 39 | # non-fatal, Flask-SQLAlchemy support is optional |
||
| 40 | # Create stub to avoid bad code later on |
||
| 41 | class FlaskSQLAlchemy(object): |
||
|
0 ignored issues
–
show
|
|||
| 42 | pass |
||
| 43 | |||
| 44 | 1 | from .model import _generate_model |
|
| 45 | 1 | from .util.db import _import_osm_file |
|
| 46 | 1 | from .util.online import _generate_overpass_api |
|
| 47 | 1 | from .triggers import _generate_triggers |
|
| 48 | |||
| 49 | 1 | class OSMAlchemy(object): |
|
|
0 ignored issues
–
show
|
|||
| 50 | """ Wrapper class for the OSMAlchemy model and logic |
||
| 51 | |||
| 52 | This class holds all the SQLAlchemy classes and logic that make up |
||
| 53 | OSMAlchemy. It is contained in a separate class because it is a |
||
| 54 | template that can be modified as needed by users, e.g. by using a |
||
| 55 | different table prefix or a different declarative base. |
||
| 56 | """ |
||
| 57 | |||
| 58 | 1 | def __init__(self, sa, prefix="osm_", overpass=None, maxage=60*60*24): |
|
| 59 | """ Initialise the table definitions in the wrapper object |
||
| 60 | |||
| 61 | This function generates the OSM element classes as SQLAlchemy table |
||
| 62 | declaratives. |
||
| 63 | |||
| 64 | Positional arguments: |
||
| 65 | |||
| 66 | sa - reference to SQLAlchemy stuff; can be either of… |
||
| 67 | …an Engine instance, or… |
||
| 68 | …a tuple of (Engine, Base), or… |
||
| 69 | …a tuple of (Engine, Base, ScopedSession), or… |
||
| 70 | …a Flask-SQLAlchemy instance. |
||
| 71 | prefix - optional; prefix for table names, defaults to "osm_" |
||
| 72 | overpass - optional; API endpoint URL for Overpass API. Can be… |
||
| 73 | …None to disable loading data from Overpass (the default), or… |
||
| 74 | …True to enable the default endpoint URL, or… |
||
| 75 | …a string with a custom endpoint URL. |
||
| 76 | maxage - optional; the maximum age after which elements are refreshed from |
||
| 77 | Overpass, in seconds, defaults to 86400s (1d) |
||
| 78 | """ |
||
| 79 | |||
| 80 | # Store logger or create mock |
||
| 81 | 1 | import logging |
|
| 82 | 1 | self.logger = logging.getLogger('osmalchemy') |
|
| 83 | 1 | self.logger.addHandler(logging.NullHandler()) |
|
| 84 | |||
| 85 | # Create fields for SQLAlchemy stuff |
||
| 86 | 1 | self.base = None |
|
| 87 | self.engine = None |
||
| 88 | 1 | self.session = None |
|
| 89 | 1 | ||
| 90 | 1 | # Inspect sa argument |
|
| 91 | 1 | if isinstance(sa, tuple): |
|
| 92 | 1 | # Got tuple of (Engine, Base) or (Engine, Base, ScopedSession) |
|
| 93 | self.engine = sa[0] |
||
| 94 | 1 | self.base = sa[1] |
|
| 95 | 1 | if len(sa) == 3: |
|
| 96 | 1 | self.session = sa[2] |
|
| 97 | 1 | else: |
|
| 98 | self.session = scoped_session(sessionmaker(bind=self.engine)) |
||
| 99 | 1 | self.logger.debug("Called with (engine, base, session) tuple.") |
|
| 100 | 1 | elif isinstance(sa, Engine): |
|
| 101 | 1 | # Got a plain engine, so derive the rest from it |
|
| 102 | self.engine = sa |
||
| 103 | self.base = declarative_base(bind=self.engine) |
||
| 104 | self.session = scoped_session(sessionmaker(bind=self.engine)) |
||
| 105 | self.logger.debug("Called with a plain SQLAlchemy engine.") |
||
| 106 | elif isinstance(sa, FlaskSQLAlchemy): |
||
| 107 | 1 | # Got a Flask-SQLAlchemy instance, extract everything from it |
|
| 108 | self.engine = sa.engine |
||
| 109 | self.base = sa.Model |
||
| 110 | 1 | self.session = sa.session |
|
| 111 | self.logger.debug("Called with a Flask-SQLAlchemy wrapper.") |
||
| 112 | else: |
||
| 113 | # Something was passed, but none of the expected argument types |
||
| 114 | raise TypeError("Invalid argument passed to sa parameter.") |
||
| 115 | |||
| 116 | # Store prefix |
||
| 117 | self.prefix = prefix |
||
| 118 | |||
| 119 | # Store maxage |
||
| 120 | self.maxage = maxage |
||
| 121 | |||
| 122 | 1 | # Store API endpoint for Overpass |
|
| 123 | if not overpass is None: |
||
| 124 | if overpass is True: |
||
| 125 | 1 | # Use default endpoint URL from overpass module |
|
| 126 | self.overpass = _generate_overpass_api() |
||
| 127 | self.logger.debug("Overpass API enabled with default endpoint.") |
||
| 128 | elif isinstance(overpass, str): |
||
| 129 | 1 | # Pass given argument as custom URL |
|
| 130 | self.overpass = _generate_overpass_api(overpass) |
||
| 131 | self.logger.debug("Overpass API enabled with endpoint %s." % overpass) |
||
|
0 ignored issues
–
show
|
|||
| 132 | 1 | else: |
|
| 133 | # We got something unknown passed, bail out |
||
| 134 | raise TypeError("Invalid argument passed to overpass parameter.") |
||
| 135 | else: |
||
| 136 | # Do not use overpass |
||
| 137 | self.overpass = None |
||
| 138 | |||
| 139 | 1 | # Generate model and store as instance members |
|
| 140 | self.node, self.way, self.relation, self.element, self.cached_query = _generate_model(self) |
||
| 141 | self.logger.debug("Generated OSMAlchemy model with prefix %s." % prefix) |
||
|
0 ignored issues
–
show
|
|||
| 142 | |||
| 143 | # Add triggers if online functionality is enabled |
||
| 144 | if not self.overpass is None: |
||
| 145 | _generate_triggers(self) |
||
| 146 | self.logger.debug("Triggers generated and activated.") |
||
| 147 | |||
| 148 | def import_osm_file(self, path): |
||
| 149 | """ Import data from an OSM XML file into this model. |
||
| 150 | |||
| 151 | path - path to the file to import |
||
| 152 | """ |
||
| 153 | |||
| 154 | # Call utility funtion with own reference and session |
||
| 155 | _import_osm_file(self, path) |
||
| 156 | |||
| 157 | def create_api(self, api_manager): |
||
| 158 | """ Create Flask-Restless API endpoints. """ |
||
| 159 | |||
| 160 | def _expand_tags(obj): |
||
| 161 | # Type name to object mapping |
||
| 162 | _types = { |
||
| 163 | "node": self.node, |
||
|
0 ignored issues
–
show
|
|||
| 164 | "way": self.way, |
||
|
0 ignored issues
–
show
|
|||
| 165 | "relation": self.relation |
||
|
0 ignored issues
–
show
|
|||
| 166 | } |
||
|
0 ignored issues
–
show
|
|||
| 167 | |||
| 168 | # Get tags dictionary from ORM object |
||
| 169 | instance = self.session.query(_types[obj["type"]]).get(obj["element_id"]) |
||
|
0 ignored issues
–
show
|
|||
| 170 | |||
| 171 | # Fill a tag dictionary |
||
| 172 | res = {} |
||
| 173 | for key in obj["tags"]: |
||
| 174 | res[key] = instance.tags[key] |
||
| 175 | |||
| 176 | # Replace tags list with generated dictionary |
||
| 177 | obj["tags"] = res |
||
| 178 | |||
| 179 | def _cleanup(obj): |
||
| 180 | # Remove unnecessary entries from dict |
||
| 181 | del obj["osm_elements_tags"] |
||
| 182 | del obj["type"] |
||
| 183 | |||
| 184 | def _post_get(result, **_): |
||
| 185 | # Post-processor for GET |
||
| 186 | # Work-around for strange bug in Flask-Restless preventing detection |
||
| 187 | # of dictionary-like association proxies |
||
| 188 | if "objects" in result: |
||
| 189 | # This is a GET_MANY call |
||
| 190 | for obj in result["objects"]: |
||
| 191 | _expand_tags(obj) |
||
| 192 | _cleanup(obj) |
||
| 193 | else: |
||
| 194 | # This is a GET_SINGLE call |
||
| 195 | _expand_tags(result) |
||
| 196 | _cleanup(result) |
||
| 197 | |||
| 198 | # Define post-processors for all collections |
||
| 199 | postprocessors = {"GET_SINGLE": [_post_get], "GET_MANY": [_post_get]} |
||
| 200 | |||
| 201 | # Define collections for all object types |
||
| 202 | api_manager.create_api(self.node, postprocessors=postprocessors) |
||
| 203 | api_manager.create_api(self.way, postprocessors=postprocessors) |
||
| 204 | api_manager.create_api(self.relation, postprocessors=postprocessors) |
||
| 205 |
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.