source: subversion/applications/utils/change_tags/change_tags.py @ 13033

Last change on this file since 13033 was 13033, checked in by crschmidt, 11 years ago

Force a dry run. When running the dry run, record the converter hash,
so that we can confirm later that the conversion function we expected
is actually the same. Also record the expected number of changes,
and if the total is more than 1000, require a secret phrase. This is
obviously not foolproof, but is designed to keep users from shooting
off the head of the database without at least some knowledge.

File size: 12.9 KB
Line 
1#!/usr/bin/python
2
3"""change_tags.py, an OSM tool.
4
5   This application is designed to convert tags, based on a downloaded .osm
6   file. To use, open the .py file and edit the contents of the 'converter'
7   function to change the tags in the way you want to, then comment out the
8   'converter = False' line immediately after the function body. Once you have
9   done that, Typically, usage would be:
10
11       python tag_changer.py --dry-run -f file.osm --verbose
12
13   followed by  manual inspection of the output XML. At the end, it will report
14   how many total nodes/elements there are, and how many would be changed by
15   the conversion.
16
17   Once you've done that, call:
18
19       python tag_changer.py -f file.osm -u <username> -p <password>
20
21   This should iterate through and perform the updates against the API,
22   reporting skipped nodes or errors at the end.
23
24   If you are working only with your own data, you should usually use
25   
26       --only-mine displayName
27
28   Which will skip any updates which do not have you as the last author.
29"""   
30
31__author__  = "Christopher Schmidt <crschmidt@crschmidt.net>"
32__version__ = "0.2"
33__revision__ = "$Id$"
34
35import dbm
36import md5
37import sys, re, xml.sax
38from xml.sax.handler import ContentHandler
39
40requires = []
41
42try:
43    from xml.etree.cElementTree import Element, SubElement, tostring
44except ImportError:
45    requires.append("Requires xml.etree.cElementTree. Try Python2.5?")
46
47try:
48    import httplib2
49except ImportError, E:
50    requires.append("No httplib2! Try easy_install httplib2 (%s)" % E)
51
52def converter(tags, type):
53    """Pass in tags dict and object type. Function must return True (object
54    changed, upload to server) or False (nothing changed, don't reupload).
55    Change the tags dict in place.
56   
57    By default, this function simply returns 'False' (nothing changed);
58    however, it has some examples of what can be done as alternatives.
59
60    This is the function you should edit in order to change the tags you
61    wish to change.
62
63    By default, adds a created_by tag identifying the source of the change.
64    """
65   
66    return False
67   
68    changed = False
69   
70    if type == "node":
71        return False 
72   
73    # Fix a typo
74    if 'name' in tags and 'Playgroung' in tags['name']:
75        tags['name'] = tags['name'].replace("Playgroung", "Playground")
76        changed = True   
77   
78    # adjust a tag based on a name
79    if 'name' in tags and ('park' in tags['name'].lower() or
80       'playground' in tags['name'].lower()):
81        tags['leisure'] = 'park'
82        changed = True
83   
84    # change a key in a tag
85    if 'leisure' in tags and tags['leisure'] == 'recreation_ground':
86        del tags['leisure']
87        if not 'landuse' in tags:
88            tags['landuse'] = 'recreation_ground'
89        else:
90            tags['landuse'] = "%s; recreation_ground" % tags['landuse']
91        changed = True
92
93    tags['created_by'] = 'change_tags.py %s' % __version__
94
95    return changed   
96
97# Comment this line out once you have edited the converter above.
98converter = False
99
100#### DO NOT CHANGE CODE AFTER THIS LINE #####
101
102
103def indent(elem, level=0):
104    """Used for pretty printing XML."""
105    i = "\n" + level*"  "
106    if len(elem):
107        if not elem.text or not elem.text.strip():
108            elem.text = i + "  "
109        for e in elem:
110            indent(e, level+1)
111            if not e.tail or not e.tail.strip():
112                e.tail = i + "  "
113        if not e.tail or not e.tail.strip():
114            e.tail = i
115    else:
116        if level and (not elem.tail or not elem.tail.strip()):
117            elem.tail = i
118
119class changeTags (ContentHandler):
120   
121    def __init__ (self, converter, db=None, user=None, password=None, dry_run=None, noisy_errors=None, only_mine=None, verbose=None):
122        ContentHandler.__init__(self)
123       
124        self.converter = converter
125        self.user = user
126        self.password = password
127        self.dry_run = dry_run
128        self.noisy_errors = noisy_errors
129        self.verbose = verbose
130        self.only_mine = only_mine
131        self.db = db
132
133        self.changes = {'node': 0, 'way': 0} 
134        self.already_changed = {'node': 0, 'way': 0} 
135        self.read = {'node': 0, 'way': 0, 'tag': 0} 
136        self.errors = []
137        self.skipped = []
138       
139        if not self.dry_run:
140            if self.user and self.password:
141                self.h = httplib2.Http()
142                self.h.add_credentials(self.user, self.password)
143            else:
144                raise Exception("Username and password required.")
145
146    def upload(self, xml):
147        if self.only_mine and self.current['user'] != self.only_mine:
148            self.skipped.append({'id': self.current['id'], 'type':self.current['type'], 'reason': "User %s doesn't match." % self.current['user']})
149            return
150        db_key = "%s:%s" % (self.current['type'], self.current['id']) 
151        if self.db and self.db.has_key(db_key):
152            self.already_changed[self.current['type']] += 1
153            return
154        url = "http://api.openstreetmap.org/api/0.5/%s/%s" % (self.current['type'], self.current['id'])
155        if self.dry_run:
156            if self.verbose:
157                print "URL:  %s" % url
158                print "XML:\n%s" % xml
159            self.changes[self.current['type']] += 1
160        else:
161            try:
162                if self.verbose: 
163                    print "Opening URL %s" % url
164                resp, content = self.h.request(url, "PUT", body=xml)
165                if resp.status != '200':
166                    error = {'item': self.current, 'code': resp.status, 'data': content}
167                    if self.noisy_errors: print "Error occurred! %s" % error
168                    self.errors.append(error)
169                else:
170                    self.db[db_key] = "1" 
171                    self.changes[self.current['type']] += 1
172
173            except Exception, E:
174                error = {'item': self.current, 'code': -1, 'data': str(E)}
175                if self.noisy_errors: print "Error occurred! %s" % error
176                self.errors.append(error)
177
178    def startElement (self, name, attr):
179        if name == 'node':
180            self.current = {'type': 'node', 'id': attr['id'],
181                'lon':attr["lon"], 'lat':attr["lat"], 'tags': {}}
182        elif name == 'way':
183            self.current = {'type': 'way', 'id': attr['id'], 'nodes':[], 'tags': {}}
184        elif name =='nd' and self.current:
185            self.current['nodes'].append(attr["ref"])
186        elif name == 'tag' and self.current:
187            self.current['tags'][attr['k']] = attr['v']
188        if 'user' in attr and self.current:
189            self.current['user'] = attr['user']
190       
191        if name in ['node', 'way', 'tag']:
192            self.read[name] += 1
193
194    def endElement (self, name):
195       
196        if name == 'way':
197            new_tags = converter(self.current['tags'], 'way')
198            if new_tags:
199                osm = Element('osm', {'version': '0.5'})
200
201                parent = SubElement(osm, 'way', {'id': self.current['id']})
202                for n in self.current['nodes']:
203                    SubElement(parent, "nd", {'ref': n})
204                keys = self.current['tags'].keys()
205                keys.sort()
206                for key in keys:
207                    SubElement(parent, "tag", {'k': key, 'v': self.current['tags'][key]})
208                indent(osm)
209                self.upload(tostring(osm))
210       
211        elif name == 'node':
212            new_tags = converter(self.current['tags'], 'node')
213            if new_tags:
214                osm = Element('osm', {'version': '0.5'})
215
216                parent = SubElement(osm, 'node', {'id': self.current['id'], 'lat': self.current['lat'], 'lon': self.current['lon']})
217                keys = self.current['tags'].keys()
218                keys.sort()
219                for key in keys:
220                    SubElement(parent, "tag", {'k': key, 'v': self.current['tags'][key]})
221               
222                indent(osm)
223                self.upload(tostring(osm))
224
225
226if __name__ == "__main__":
227    if not converter:
228        if requires:
229            __doc__ = "%s\nRequired dependancies unavailable: \n %s" % (__doc__, ("\n  ".join(requires)))
230        print __doc__
231        sys.exit(1)
232   
233    if requires:
234        print "Required dependancies unavailable: \n  " % ("\n  ".join(requires)) 
235        sys.exit(2)
236
237    from optparse import OptionParser
238    parser = OptionParser()
239
240    parser.add_option("-f", "--file", help="source file. default is stdin", dest="file")
241    parser.add_option("-d", "--dry-run", action="store_true", default=False, help="print URLs and XML for changed items, rather than actually changing them.", dest="dry_run")
242    parser.add_option("-u", "--username", help="username for OSM API", dest="username")
243    parser.add_option("-p", "--password", help="api password (will prompt if not provided and required)", dest="password")
244    parser.add_option("-e", "--noisy-errors", dest="noisy_errors", default=False, action="store_true")
245    parser.add_option("--verbose", help="be verbose", dest="verbose", default=False, action="store_true")
246    parser.add_option('-o', "--only-mine", dest="only_mine", help="Provide a username/displayname which will be used to check if an edit should be perormed.") 
247    parser.add_option("-n", "--no-status", dest="no_status", action="store_true", default=False, help="Don't store status db for recovery of upload (faster; riskier")
248    parser.add_option('--profile', dest='profile', action="store_true", help="Report profiler stats", default=False)
249
250    options, args = parser.parse_args()
251
252    f = None
253    if options.file:
254        f = open(options.file)
255        if not options.no_status:
256            db = dbm.open("%s.db" % options.file, "c")
257        else:
258            db = None 
259    else:
260        raise Exception("No input file")
261    #    f = sys.stdin
262    #    db=None
263    #    if not options.no_status:
264    #        print "Using stdin, unable to create status db; "
265    if not options.dry_run and options.username and not options.password:
266        import getpass
267        options.password = getpass.getpass("Password: ") 
268   
269    if not options.dry_run:
270        try:
271            f_dry = open("%s.dry_run" % options.file)
272        except Exception, E:
273            print "You haven't run a dry run to see what the results will be! (%s)" % E
274            sys.exit(2)
275        data = f_dry.read().strip()
276        hash, count = data.split("\n|||\n")
277        if hash != converter.func_code.co_code:
278            print "The converter function changed, and you haven't run another dry run. Do that first."
279            sys.exit(3)
280        if int(count) > 1000:
281            print "You are changing more than 1000 objects. Ask crschmidt for the special password."
282            pw = raw_input("Secret Phrase: ")
283            if md5.md5(pw).hexdigest() != '1271ed5ef305aadabc605b1609e24c52': 
284                print "That's too much at once. You might break something. %s" % count
285                sys.exit(4)
286           
287           
288    osmParser = changeTags(
289            converter,
290            db=db,
291            user=options.username, 
292            password=options.password, 
293            dry_run=options.dry_run, 
294            noisy_errors = options.noisy_errors, 
295            only_mine=options.only_mine,
296            verbose=options.verbose)
297   
298    prof = None
299    if options.profile:
300        import hotshot, hotshot.stats
301        prof = hotshot.Profile("tagChanger.prof")
302   
303    try:
304        if prof:
305            prof.runcall(xml.sax.parse, f, osmParser)
306        else:   
307            xml.sax.parse( f, osmParser )
308    except KeyboardInterrupt:
309        print "\nStopping at %s %s due to interrupt"  % (osmParser.current['type'], osmParser.current['id'])
310        pass
311    except Exception, E:
312        print E
313        print "\nStopping at %s %s due to exception: \n%s"  % (osmParser.current['type'], osmParser.current['id'], E)
314
315    print "Total Read: %s nodes, %s ways, %s tags"  % (osmParser.read['node'], osmParser.read['way'], osmParser.read['tag'])
316    print "Total Changed: %s nodes, %s ways"  % (osmParser.changes['node'], osmParser.changes['way'])
317    print "Previously Changed: %s nodes, %s ways"  % (osmParser.already_changed['node'], osmParser.already_changed['way'])
318    if options.dry_run and options.file:
319        f = open("%s.dry_run" % options.file, "w")
320        f.write("%s\n|||\n%s" % (converter.func_code.co_code, osmParser.changes['node'] + osmParser.changes['way']))
321        f.close()
322    if len(osmParser.errors):
323        print "The following %s errors occurred:" % len(osmParser.errors)
324    for e in osmParser.errors:
325         print "%s: %s. Code: %s. Error Text: %s" % (e['item']['type'],e['item']['id'], e['code'], e['data'])
326    if len(osmParser.skipped):
327        print "The following %s items were skipped:" % len(osmParser.skipped)
328    for e in osmParser.skipped:
329         print "%s: %s. Reason: %s" % (e['type'],e['id'], e['reason'])
330    if prof:
331        stats = hotshot.stats.load("tagChanger.prof")
332        stats.strip_dirs()
333        stats.sort_stats('time', 'calls')
334        stats.print_stats(20)
Note: See TracBrowser for help on using the repository browser.