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.