Issues (77)

osmalchemy/triggers.py (2 issues)

1
# ~*~ coding: utf-8 ~*~
2
#-
3
# OSMAlchemy - OpenStreetMap to SQLAlchemy bridge
4
# Copyright (c) 2016 Dominik George <[email protected]>
5
# Copyright (c) 2016 Eike Tim Jesinghaus <[email protected]>
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included in all
15
# copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
# SOFTWARE.
24
#
25
# Alternatively, you are free to use OSMAlchemy under Simplified BSD, The
26
# MirOS Licence, GPL-2+, LGPL-2.1+, AGPL-3+ or the same terms as Python
27
# itself.
28 1
29
""" Trigger code for live OSMAlchemy/Overpass integration. """
30 1
31 1
import datetime
32 1
from hashlib import md5
33 1
from sqlalchemy import inspect
34 1
from sqlalchemy.event import listens_for
35
from sqlalchemy.orm import Query
36 1
from weakref import WeakSet
37 1
38
from .util.db import _import_osm_xml
39 1
from .util.online import (_get_elements_by_query, _where_to_tree,
40
                          _trees_to_overpassql, _normalise_overpassql,
41
                          _get_single_element_by_id)
42
43
def _generate_triggers(oa):
44
    """ Generates the triggers for online functionality.
45
46
      oa - reference to the OSMAlchemy instance to be configured
47
    """
48
49
    _visited_queries = WeakSet()
50
51
    @listens_for(oa.node, "load")
52
    @listens_for(oa.way, "load")
53
    @listens_for(oa.relation, "load")
54
    def _instance_loading(target, context, _=None):
55
        # Get query session
56
        session = context.session
57
58
        # Skip if the session was in a trigger before
59
        # Prevents recursion in import code
60
        if hasattr(session, "_osmalchemy_in_trigger"):
61
            return
62
63
        # Determine OSM element type and id
64
        type_ = target.__class__.__name__[3:].lower()
65
        id_ = target.id
66
67
68
        # Guard against broken objects without an id
69
        if id_ is None:
70
            return
71
72
        oa.logger.debug("Loading instance of type %s with id %i." % (type_, id_))
73
74
        # Check whether object needs to be refreshed
75
        updated = target.osmalchemy_updated
76
        timediff = datetime.datetime.now() - updated
77
        if timediff.total_seconds() < oa.maxage:
78
            oa.logger.debug("Instance is only %i seconds old, not refreshing online." % timediff.total_seconds())
79
            return
80
81
        oa.logger.debug("Refreshing instance online.")
82
83
        # Get object by id as XML
84
        xml = _get_single_element_by_id(oa.overpass, type_, id_)
85
86
        # Import data
87
        session._osmalchemy_in_trigger = True
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _osmalchemy_in_trigger was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
88
        _import_osm_xml(oa, xml, session=session)
89
        del session._osmalchemy_in_trigger
90
91
    @listens_for(Query, "before_compile")
92
    def _query_compiling(query):
93
        # Get the session associated with the query:
94
        session = query.session
95
96
        # Skip if the session was in a trigger before
97
        # Prevents recursion in import code
98
        if hasattr(session, "_osmalchemy_in_trigger"):
99
            return
100
101
        # Prevent recursion by skipping already-seen queries
102
        if query in _visited_queries:
103
            return
104
        else:
105
            _visited_queries.add(query)
106
107
        oa.logger.debug("Analysing new ORM query.")
108
109
        # Check whether this query affects our model
110
        affected_models = set([c["type"] for c in query.column_descriptions])
111
        our_models = set([oa.node, oa.way,  oa.relation,
112
                          oa.element])
113
        if affected_models.isdisjoint(our_models):
114
            # None of our models is affected
115
            oa.logger.debug("None of our models are affected.")
116
            return
117
118
        # Check whether this query filters elements
119
        # Online update will only run on a specified set, not all data
120
        if query.whereclause is None:
121
            # No filters
122
            oa.logger.debug("No filters found in query.")
123
            return
124
125
        oa.logger.debug("Building query tree from ORM query structure.")
126
127
        # Analyse where clause looking for all looked-up fields
128
        trees = {}
129
        for target in our_models.intersection(affected_models):
130
            # Build expression trees first
131
            tree = _where_to_tree(query.whereclause, target)
132
            if not tree is None:
133
                trees[target.__name__] = tree
134
135
        # Bail out if no relevant trees were built
136
        if not trees:
137
            oa.logger.debug("No relevant query trees built.")
138
            return
139
140
        # Compile to OverpassQL
141
        oql = _trees_to_overpassql(trees)
142
        oa.logger.debug("Compiled OverpassQL: %s" % oql)
143
144
        # Look up query in cache
145
        hashed_oql = md5(_normalise_overpassql(oql).encode()).hexdigest()
146
        cached_query = session.query(oa.cached_query).filter_by(oql_hash=hashed_oql).scalar()
147
        # Check age if cached query was found
148
        if cached_query:
149
            timediff = datetime.datetime.now() - cached_query.oql_queried
150
            if timediff.total_seconds() < oa.maxage:
151
                # Return and do nothing if query was run recently
152
                oa.logger.debug("Query was run online only %i seconds ago, not running." % timediff.total_seconds())
153
                return
154
155
        # Run query online
156
        oa.logger.debug("Running Overpass query.")
157
        xml = _get_elements_by_query(oa.overpass, oql)
158
159
        # Import data
160
        session._osmalchemy_in_trigger = True
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _osmalchemy_in_trigger was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
161
        _import_osm_xml(oa, xml, session=session)
162
        del session._osmalchemy_in_trigger
163
164
        # Store or update query time
165
        if not cached_query:
166
            cached_query = oa.cached_query()
167
            cached_query.oql_hash = hashed_oql
168
        cached_query.oql_queried = datetime.datetime.now()
169
        session.add(cached_query)
170
        session.commit()
171
        oa.logger.debug("Query cached.")
172