--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/diff.py Fri Nov 10 11:52:37 2017 +0800
+from argparse import ArgumentParser
+from difflib import unified_diff
+from subprocess import run
+from tempfile import NamedTemporaryFile
+from urllib.request import urlopen
+from xml.etree import ElementTree as ET
+API = 'https://api.openstreetmap.org/api/0.6'
+def fetch_xml(url, what, n, total, delay=200, spins=r'/-\|'):
+ stderr.write('\r{} {} {} out of {}'.format(spins[n % len(spins)], what, n, total))
+ with urlopen(url) as response:
+ tree = ET.parse(response)
+def parse_item_xml(kind, el):
+ tag.get('k'): tag.get('v')
+ for tag in el.findall('tag')
+ (ref.tag, ref.get('ref'), None)
+ for ref in el.findall('*[@ref]')
+ elif kind == 'relation':
+ (member.get('type'), member.get('ref'), member.get('role'))
+ for member in el.findall('member')
+ items = {'node': {}, 'way': {}, 'relation': {}}
+ # changeset metadata mostly
+ xml = fetch_xml('{}/changeset/{}'.format(API, csid), 'changeset data', 1, 2, delay=0)
+ 'attrs': xml.find('changeset').attrib,
+ tag.get('k'): tag.get('v')
+ for tag in xml.find('changeset').findall('tag')
+ # current versions of nodes, ways, relations
+ xml = fetch_xml('{}/changeset/{}/download'.format(API, csid), 'changeset data', 2, 2)
+ if node.tag not in ('create', 'modify', 'delete'):
+ if el.tag not in ('node', 'way', 'relation'):
+ version = int(el.get('version'))
+ version: parse_item_xml(el.tag, el)
+ # fetch previous versions
+ for kind in ('node', 'way', 'relation'):
+ total = len(items[kind])
+ for n, id in enumerate(items[kind], 1):
+ version = min(items[kind][id])
+ xml = fetch_xml('{}/{}/{}/{}'.format(API, kind, id, version), kind + 's', n, total)
+ items[kind][id][version] = parse_item_xml(kind, xml.find(kind))
+ return changeset, items
+def level0lify(changeset, items, refmap={'node': 'nd', 'way': 'wy'}):
+ a.append('# previous changeset is most likely unrelated')
+ b.append('changeset {}'.format(changeset['attrs']['id']))
+ b += [' {} = {}'.format(k, v) for k, v in sorted(changeset['tags'].items())]
+ for kind in ('relation', 'way', 'node'):
+ for id in sorted(items[kind]):
+ versions = items[kind][id]
+ for lines, version in ((a, old), (b, new)):
+ lines.append('{} {}'.format(kind, id))
+ ' {} = {}'.format(k, v)
+ for k, v in sorted(versions[version]['tags'].items())
+ ' {} {}{}'.format(refmap.get(kind, kind), id, ' ' + role if role is not None else '')
+ for kind, id, role in versions[version]['refs']
+ parser = ArgumentParser()
+ parser.add_argument('changeset', type=int, help='changeset id')
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('-U', '--unified', type=int, help='lines of context')
+ group.add_argument('-t', '--tool', help='external tool to use for diffing')
+ args = parser.parse_args()
+ changeset, items = bootstrap(args.changeset)
+ a, b = level0lify(changeset, items)
+ context = args.unified if args.unified is not None else 3
+ diff = unified_diff(a, b, n=context, lineterm='')
+ with NamedTemporaryFile() as afile, NamedTemporaryFile() as bfile:
+ for fd, lines in ((afile, a), (bfile, b)):
+ fd.write('\n'.join(lines).encode('utf-8'))
+ run([args.tool, afile.name, bfile.name])
+if __name__ == '__main__':