source: trunk/npemap.org.uk/scripts/generic-python-import/generic_importer.py

Last change on this file was 755, checked in by Dominic Hargreaves, 7 years ago

catch errors from wget

File size: 12.4 KB
Line 
1#!/usr/bin/python
2#
3# Copyright (c) 2006-2007 Nick Burch and Dominic Hargreveaves
4# Permission is hereby granted, free of charge, to any person obtaining a
5# copy of this software and associated documentation files (the "Software"),
6# to deal in the Software without restriction, including without limitation
7# the rights to use, copy, modify, merge, publish, distribute, sublicense,
8# and/or sell copies of the Software, and to permit persons to whom the
9# Software is furnished to do so, subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be included in
12# all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21#
22#               External Datafeed Importer
23#               --------------------------
24#
25# Imports external data into the current schema, removing any
26#  existing data from that source.
27#
28# You will need to tweak this script with your database settings for it
29#  to work.
30
31from geo_helper import turn_wgs84_into_osgb36, turn_osgb36_into_eastingnorthing, \
32                                                turn_wgs84_into_osie36, turn_osie36_into_eastingnorthing
33import psycopg2
34import os
35import sys
36import re
37
38# Database settings
39dbtype = "postgres"
40dbname = "npemap"
41dbhost = ""
42dbuser = "npemap"
43dbpass = ""
44
45class Importer(object):
46        "Parent class of all generic importers"
47        def __init__(self,source_name,delete_reason,url):
48                self.source_name = source_name
49                self.delete_reason = delete_reason
50                self.url = url
51
52                self.verbose = False
53                self.download = False
54                self.confirm_update = True
55
56        def generate_easting_northing(self,pc):
57                "Handle the lat+long => e+n bit"
58                if not pc:
59                        return
60                pc["easting"] = None
61                pc["northing"] = None
62                pc["ie_easting"] = None
63                pc["ie_northing"] = None
64
65                # Turn lat+long into easting+northing
66                # All NI postcodes are BT
67                if pc["outer"][0:2] == 'BT':
68                        osll = turn_wgs84_into_osie36(pc["latitude"],pc["longitude"], 0)
69                        en = turn_osie36_into_eastingnorthing(osll[0], osll[1])
70                        pc["ie_easting"] = en[0]
71                        pc["ie_northing"] = en[1]
72                else:
73                        osll = turn_wgs84_into_osgb36(pc["latitude"],pc["longitude"], 0)
74                        en = turn_osgb36_into_eastingnorthing(osll[0], osll[1])
75                        pc["easting"] = en[0]
76                        pc["northing"] = en[1]
77
78        def handle_arguments(self, argv):
79                "What arguments did they pass in?"
80                for arg in argv[1:]: 
81                        if arg == "--verbose":
82                                self.verbose = True
83                        if arg == "--download":
84                                self.download = True
85                        if arg == "--no-confirm":
86                                self.confirm_update = False
87
88        def process(self):
89                "Do the main processing loop"
90
91                # Connect to the database
92                dbh = None
93                if dbtype == "pgsql" or dbtype == "postgres" or dbtype == "postgresql":
94                        if len(dbhost):
95                                dbh = psycopg2.connect(database=dbname, host=dbhost, user=dbuser, password=dbpass)
96                        else:
97                                dbh = psycopg2.connect(database=dbname, user=dbuser, password=dbpass)
98                else:
99                        raise Exception("Unknown dbtype %s" % dbtype)
100
101
102                # Check what source value the data will have
103                source_id = None
104
105                sql = "SELECT id FROM sources WHERE name = %(name)s"
106                sth = dbh.cursor()
107                sth.execute(sql, {'name':self.source_name})
108                ids = sth.fetchall()
109                sth.close()
110                if len(ids) == 1:
111                        for id in (ids):
112                                source_id = id[0]
113                else:
114                        print "Unable to find ID for source '%s' - error code %d" % (self.source_name, len(ids))
115                        print "Please add it to the database"
116                        return
117
118
119                # See if our delete reason exists, and if no, add it
120                reason_id = None
121                while reason_id == None:
122                        sql = "SELECT id FROM delete_reasons WHERE reason = %(reason)s"
123                        sth = dbh.cursor()
124                        sth.execute(sql, {'reason':self.delete_reason})
125                        ids = sth.fetchall()
126                        sth.close()
127                        if len(ids) == 1:
128                                for id in (ids):
129                                        reason_id = id[0]
130                        else:
131                                sth = dbh.cursor()
132                                sth.execute("INSERT INTO delete_reasons (reason) VALUES (%(reason)s)", {'reason':self.delete_reason})
133                                sth.close()
134
135
136                # Download the latest list of postcodes if needed
137                current_file = None
138                if os.path.isfile("currentlist"):
139                        current_file = "currentlist"
140                if os.path.isfile("/tmp/currentlist"):
141                        current_file = "/tmp/currentlist"
142
143                if not self.download and not current_file == None:
144                        print "Data found, do you wish to re-download?"
145                        redownload = raw_input("")
146                        if redownload == "y" or redownload == "yes":
147                                download = True
148                else:
149                        self.download = True
150
151                if self.download:
152                        if self.verbose:
153                                print "Downloading from %s" % self.url
154                        if os.system("wget --quiet -O currentlist '%s'" % self.url) != 0:
155                                raise Exception("Download from %s failed" % self.url)
156                        current_file = "currentlist"
157                        if self.verbose:
158                                print ""
159
160
161                # Read in the new list
162                raw_postcodes = {}
163                ftpc = open(current_file, 'r')
164                for line in ftpc:
165                        pc = self.process_line(line)
166                        if pc:
167                                raw_pc = pc["raw"]
168                                if not raw_postcodes.has_key(raw_pc):
169                                        raw_postcodes[raw_pc] = []
170                                raw_postcodes[raw_pc].append(pc)
171
172                # Ensure that the postcodes entries are unique, and
173                #  average them if not
174                postcodes = []
175                for raw_pc, arr_data in raw_postcodes.items():
176                        if len(arr_data) == 1:
177                                # Only the one, easy
178                                pc = arr_data[0]
179                        else:
180                                # Several, average
181                                lat = 0
182                                lng = 0
183                                for pc in arr_data:
184                                        lat += float(pc["latitude"])
185                                        lng += float(pc["longitude"])
186                                # Create the average
187                                pc = arr_data[0]
188                                pc["latitude"]  = lat / len(arr_data)
189                                pc["longitude"] = lng / len(arr_data)
190                                self.generate_easting_northing(pc)
191                        # Save the now always-unique postcode
192                        postcodes.append( pc )
193
194
195                # Grab all of the current ones, including deleted ones
196                # Make sure that un-deleted ones come first, so that if one postcode
197                #  has deleted and undeleted forms, we prefer the undeleted form
198                sql = "SELECT id, outward, inward, deleted, delete_reason FROM postcodes WHERE source = %(source)s ORDER BY deleted"
199                sth = dbh.cursor()
200                sth.execute(sql, {'source':source_id})
201
202                spcs = {}
203                while 1:
204                        row = sth.fetchone()
205                        if row == None:
206                                break
207                        id, outward, inward, deleted, delete_reason = row
208
209                        postcode = "%s %s" % (outward, inward)
210                        if not spcs.has_key(postcode):
211                                spcs[postcode] = { 
212                                        'outward':outward, 'inward':inward, 'id':id, 'done':False,
213                                        'deleted':deleted, 'delete_reason': delete_reason, }
214                sth.close()
215                count = len(spcs.keys())
216                deleted_count = len([a for a in spcs.values() if a['deleted']])
217                undeleted_count = count - deleted_count
218
219                # And for interest, grab the raw deleted count
220                sql = "SELECT COUNT(id) FROM postcodes WHERE source = %(source)s AND deleted"
221                sth = dbh.cursor()
222                sth.execute(sql, {'source':source_id})
223                raw_deleted_count = sth.fetchone()[0]
224                sth.close()
225
226                print "There are currently %d distinct entries in the database from the %s." % (count, self.source_name)
227                print " Of these, %d are live, and %d are deleted" % (undeleted_count, deleted_count)
228                print " There are also %d raw deleted entries (may include duplicates)" % (raw_deleted_count)
229                print "The new import contains %d entries" % len(postcodes)
230
231                # Only prompt if it's a big difference
232                if self.confirm_update:
233                        if (undeleted_count == 0 or len(postcodes) == 0 or 
234                                        len(postcodes) < undeleted_count or (len(postcodes)-undeleted_count) > 50):
235                                print "Are you sure you wish to run an import?"
236                                confirm = raw_input("")
237                                print ""
238
239                                if confirm == "y" or confirm == "yes":
240                                        # Good, go ahead
241                                        pass
242                                else:
243                                        print ""
244                                        raise Exception("Aborting import")
245                else:
246                        # Don't trash everything even with --no-confirm
247                        if len(postcodes) == 0 or abs(len(postcodes)-undeleted_count) > 100:
248                                raise Exception("Postcode count too different, not running (re-run without --no-confirm to allow")
249
250
251                # Add the latest list to the database, tweaking if already there
252                add_sql = "INSERT INTO postcodes (outward, inward, raw_postcode_outward, raw_postcode_inward, easting, northing, ie_easting, ie_northing, source) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)"
253                upd_sql = "UPDATE postcodes SET outward=%s, inward=%s, raw_postcode_outward=%s, raw_postcode_inward=%s, easting=%s, northing=%s, ie_easting=%s, ie_northing=%s, deleted='f', delete_reason=NULL WHERE id=%s"
254                del_sql = "UPDATE postcodes SET deleted='t', delete_reason=%s WHERE id=%s"
255                sth = dbh.cursor()
256                worked = 0
257
258                # Add the postcodes
259                for postcode in postcodes:
260                        pc = "%s %s" % (postcode["outer"], postcode["inner"])
261                        if self.verbose:
262                                print "Processing %s" % pc
263
264                        if spcs.has_key(pc):
265                                # If it's previously been deleted, just skip it, unless
266                                #  it's one that went and came back
267                                if spcs[pc]['deleted']:
268                                        if spcs[pc]['delete_reason'] == reason_id:
269                                                if self.verbose:
270                                                        print "\tPreviously missing postcode has returned (id %d)" % spcs[pc]['id']
271                                        else:
272                                                if self.verbose:
273                                                        print "\tSkipping as previously deleted - %s (id %d)" % (spcs[pc]['delete_reason'], spcs[pc]['id'])
274                                                continue
275
276                                # Otherwise, update the existing entry
277                                spcs[pc]['done'] = True
278                                sth.execute(upd_sql, (postcode["outer"], postcode["inner"], postcode["raw_outer"], postcode["raw_inner"], postcode["easting"], postcode["northing"], postcode["ie_easting"], postcode["ie_northing"], spcs[pc]['id']))
279                                if self.verbose:
280                                        print "\tupdated record at %d" % spcs[pc]['id']
281                        else:
282                                # New record, add
283                                sth.execute(add_sql, (postcode["outer"], postcode["inner"], postcode["raw_outer"], postcode["raw_inner"], postcode["easting"], postcode["northing"], postcode["ie_easting"], postcode["ie_northing"], source_id))
284                                if self.verbose:
285                                        print "\tadded new postcode"
286                                else:
287                                        print "Added postcode %s" % pc
288                        worked = worked + 1
289
290                print "Processed %d entries" % worked
291
292
293                # Find ones that have gone
294                if self.verbose:
295                        print "\nDeleting any postcodes no longer in source:"
296                gone = [ pc for pc in spcs.keys() if not spcs[pc]['done'] and not spcs[pc]['deleted'] ]
297                for gone_pc in gone:
298                        print "\tflagging as deleted old postcode %s" % gone_pc
299                        sth.execute(del_sql, (reason_id, spcs[gone_pc]['id']))
300
301                # All done
302                if self.verbose:
303                        print "\nAll Done"
304                sth.close()
305                dbh.commit()
306                dbh.close()
307
308class FreeThePostcodeImporter(Importer):
309        "An importer for FreeThePostcode.org"
310        def __init__(self):
311                super(FreeThePostcodeImporter, self).__init__(
312                        source_name="FreeThePostcode.org Importer",
313                        delete_reason="Gone from FTP",
314                        url="http://www.freethepostcode.org/currentlist"
315                )
316
317        def process_line(self,line):
318                "Process one line of the file"
319                line = line[0:-1]
320                if line[0:1] == "#":
321                        return
322
323                parts = re.split(" +", line)
324                if not len(parts) == 4:
325                        print "Invalid line '%s'" % line
326                        return
327
328                pc = {}
329                pc["outer"] = parts[2]
330                pc["inner"] = parts[3]
331                pc["raw"] = "%s %s" % (parts[2],parts[3])
332                pc["raw_outer"] = parts[2]
333                pc["raw_inner"] = parts[3]
334                pc["latitude"] = parts[0]
335                pc["longitude"] = parts[1]
336
337                self.generate_easting_northing(pc)
338
339                # All done
340                return pc
341
342class DracosPostboxImporter(Importer):
343        "An importer for the postcode tagged postbox data from dracos.co.uk"
344
345        def __init__(self):
346                super(DracosPostboxImporter, self).__init__(
347                        source_name="Dracos.co.uk Postbox Importer",
348                        delete_reason="Gone from Dracos Postboxes",
349                        url="http://www.dracos.co.uk/play/locating-postboxes/export.php?rm=1"
350                )
351                self.valid_pc = re.compile("^([A-Z]+\d+\s*[A-Z]?)\s*(\d[A-Z][A-Z])$")
352
353        def process_line(self,line):
354                "Process one line of the file"
355                line = line[0:-1]
356                if line[0:1] == "#":
357                        return
358                if line.startswith("Ref"):
359                        return
360
361                # Ref, Postcode, Loc1, Loc2, Latitude, Longitude, last m-f, last sat source
362                parts = line.split("\t")
363                if len(parts) == 9:
364                        pass
365                else:
366                        print "Invalid line '%s'" % line
367                        return
368
369                if not parts[1]:
370                        # No postcode
371                        return
372                if not parts[8] == "Website":
373                        # Don't want OSM data, it's not free enough.
374                        return
375
376                # Does the postcode part look valid?
377                match = self.valid_pc.match(parts[1])
378                if not match:
379                        #print "Invalid - %s" % parts[1]
380                        return
381                outer, inner = match.groups()
382
383                # Seems to be things like "W1 C2DN" not "W1C 2DN", fix
384                outer = outer.replace(" ", "")
385                #print "%s ** %s" % (outer,inner)
386
387                pc = {}
388                pc["raw"] = parts[1]
389                pc["outer"] = outer
390                pc["inner"] = inner
391                pc["raw_outer"] = outer
392                pc["raw_inner"] = inner
393                pc["latitude"] = parts[4]
394                pc["longitude"] = parts[5]
395
396                self.generate_easting_northing(pc)
397
398                # All done
399                return pc
Note: See TracBrowser for help on using the repository browser.