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

Last change on this file since 648 was 648, checked in by Nick Burch, 11 years ago

Refactor the FTP importer

File size: 8.5 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 pyPgSQL import PgSQL
32from geo_helper import turn_wgs84_into_osgb36, turn_osgb36_into_eastingnorthing, \
33                                                turn_wgs84_into_osie36, turn_osie36_into_eastingnorthing
34import os
35import sys
36
37# Database settings
38dbtype = "postgres"
39dbname = "npemap"
40dbhost = ""
41dbuser = "npemap"
42dbpass = ""
43
44class Importer(object):
45        "Parent class of all generic importers"
46        def __init__(self,source_name,delete_reason,url):
47                self.source_name = source_name
48                self.delete_reason = delete_reason
49                self.url = url
50
51                self.verbose = False
52                self.download = False
53                self.confirm_update = True
54
55        def handle_arguments(self, argv):
56                "What arguments did they pass in?"
57                for arg in argv[1:]: 
58                        if arg == "--verbose":
59                                self.verbose = True
60                        if arg == "--download":
61                                self.download = True
62                        if arg == "--no-confirm":
63                                self.confirm_update = False
64
65        def process(self):
66                "Do the main processing loop"
67
68                # Connect to the database
69                dbh = None
70                if dbtype == "pgsql" or dbtype == "postgres" or dbtype == "postgresql":
71                        if len(dbhost):
72                                dbh = PgSQL.connect(database=dbname, host=dbhost, user=dbuser, password=dbpass)
73                        else:
74                                dbh = PgSQL.connect(database=dbname, user=dbuser, password=dbpass)
75                else:
76                        raise Exception("Unknown dbtype %s" % dbtype)
77
78
79                # Check what source value the data will have
80                source_id = None
81
82                sql = "SELECT id FROM sources WHERE name = %s"
83                sth = dbh.cursor()
84                sth.execute(sql, self.source_name)
85                ids = sth.fetchall()
86                sth.close()
87                if len(ids) == 1:
88                        for id in (ids):
89                                source_id = id.id
90                else:
91                        print "Unable to find ID for source '%s' - error code %d" % (self.source_name, len(ids))
92
93
94                # See if our delete reason exists, and if no, add it
95                reason_id = None
96                while reason_id == None:
97                        sql = "SELECT id FROM delete_reasons WHERE reason = %s"
98                        sth = dbh.cursor()
99                        sth.execute(sql, self.delete_reason)
100                        ids = sth.fetchall()
101                        sth.close()
102                        if len(ids) == 1:
103                                for id in (ids):
104                                        reason_id = id.id
105                        else:
106                                sth = dbh.cursor()
107                                sth.execute("INSERT INTO delete_reasons (reason) VALUES (%s)", self.delete_reason)
108                                sth.close()
109
110
111                # Download the latest list of postcodes if needed
112                current_file = None
113                if os.path.isfile("currentlist"):
114                        current_file = "currentlist"
115                if os.path.isfile("/tmp/currentlist"):
116                        current_file = "/tmp/currentlist"
117
118                if not self.download and not current_file == None:
119                        print "Data found, do you wish to re-download?"
120                        redownload = raw_input("")
121                        if redownload == "y" or redownload == "yes":
122                                download = True
123                else:
124                        self.download = True
125
126                if self.download:
127                        if verbose:
128                                print "Downloading from %s" % self.url
129                        os.system("wget --quiet -O currentlist '%s'" % self.url)
130                        current_file = "currentlist"
131                        if verbose:
132                                print ""
133
134
135                # Read in the new list
136                postcodes = []
137                ftpc = open(current_file, 'r')
138                for line in ftpc:
139                        pc = self.process_line(line)
140                        if pc:
141                                postcodes.append(pc)
142
143
144                # Grab all of the current ones
145                sql = "SELECT id, outward, inward, deleted FROM postcodes WHERE source = %s AND NOT deleted"
146                sth = dbh.cursor()
147                sth.execute(sql, source_id)
148
149                spcs = {}
150                while 1:
151                        row = sth.fetchone()
152                        if row == None:
153                                break
154                        id, outward, inward, deleted = row
155                        spcs["%s %s" % (outward, inward)] = \
156                                                { 'outward':outward, 'inward':inward, 'id':id, 'deleted':deleted, 'done':False }
157                sth.close()
158                count = len(spcs.keys())
159
160                print "There are currently %d entries in the database from %s" % (count, self.source_name)
161                print "The new import contains %d entries" % len(postcodes)
162
163                # Only prompt if it's a big difference
164                if self.confirm_update:
165                        if (count == 0 or len(postcodes) == 0 or 
166                                        len(postcodes) < count or (len(postcodes)-count) > 50):
167                                print "Are you sure you wish to run an import?"
168                                confirm = raw_input("")
169                                print ""
170
171                                if confirm == "y" or confirm == "yes":
172                                        # Good, go ahead
173                                        pass
174                                else:
175                                        print ""
176                                        raise Exception("Aborting import")
177                else:
178                        # Don't trash everything even with --no-confirm
179                        if len(postcodes) == 0 or abs(len(postcodes)-count) > 100:
180                                raise Exception("Postcode count too different, not running (re-run without --no-confirm to allow")
181
182
183                # Add the latest list to the database, tweaking if already there
184                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)"
185                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 WHERE id=%s"
186                del_sql = "UPDATE postcodes SET deleted='t', delete_reason=%s WHERE id=%s"
187                sth = dbh.cursor()
188                worked = 0
189
190                # Add the postcodes
191                for postcode in postcodes:
192                        pc = "%s %s" % (postcode["outer"], postcode["inner"])
193                        if self.verbose:
194                                print "Processing %s" % pc
195
196                        if spcs.has_key(pc):
197                                # Update existing one
198                                spcs[pc]['done'] = True
199                                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']))
200                                if self.verbose:
201                                        print "\tupdated record at %d" % spcs[pc]['id']
202                        else:
203                                # New record, add
204                                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))
205                                if self.verbose:
206                                        print "\tadded new postcode"
207                                else:
208                                        print "Added postcode %s" % pc
209                        worked = worked + 1
210
211                print "Processed %d entries" % worked
212
213
214                # Find ones that have gone
215                if self.verbose:
216                        print "\nDeleting any postcodes no longer in source:"
217                for gone_pc in [ pc for pc in spcs.keys() if not spcs[pc]['done'] ]:
218                        print "\tflagging as deleted old postcode %s" % gone_pc
219                        sth.execute(del_sql, (reason_id, spcs[gone_pc]['id']))
220
221                # All done
222                if self.verbose:
223                        print "\nAll Done"
224                sth.close()
225                dbh.commit()
226                dbh.close()
227
228class FreeThePostcodeImporter(Importer):
229        "An importer for FreeThePostcode.org"
230        def __init__(self):
231                super(FreeThePostcodeImporter, self).__init__(
232                        source_name="FreeThePostcode.org Importer",
233                        delete_reason="Gone from FTP",
234                        url="http://www.freethepostcode.org/currentlist"
235                )
236
237        def process_line(self,line):
238                "Process one line of the file"
239                line = line[0:-1]
240                if line[0:1] == "#":
241                        return
242
243                # Replace double spaces
244                line = line.replace("  "," ")
245
246                parts = line.split(" ")
247                if not len(parts) == 4:
248                        print "Invalid line '%s'" % line
249                        return
250
251                pc = {}
252                pc["outer"] = parts[2]
253                pc["inner"] = parts[3]
254                pc["raw"] = "%s %s" % (parts[2],parts[3])
255                pc["raw_outer"] = parts[2]
256                pc["raw_inner"] = parts[3]
257
258                pc["easting"] = None
259                pc["northing"] = None
260                pc["ie_easting"] = None
261                pc["ie_northing"] = None
262
263                # Turn lat+long into easting+northing
264                # All NI postcodes are BT
265                if pc["outer"][0:2] == 'BT':
266                        osll = turn_wgs84_into_osie36(parts[0], parts[1], 0)
267                        en = turn_osie36_into_eastingnorthing(osll[0], osll[1])
268                        pc["ie_easting"] = en[0]
269                        pc["ie_northing"] = en[1]
270                else:
271                        osll = turn_wgs84_into_osgb36(parts[0], parts[1], 0)
272                        en = turn_osgb36_into_eastingnorthing(osll[0], osll[1])
273                        pc["easting"] = en[0]
274                        pc["northing"] = en[1]
275
276                # All done
277                return pc
Note: See TracBrowser for help on using the repository browser.