3:e30c88afe8aa draft default tip
Anton Shestakov <av6@dwimlabs.net>, Sun, 12 Nov 2017 14:28:14 +0800
level0lify: skip invisible items

previous change 2:02c80a0ce839

diff.py

Permissions: -rwxr-xr-x

Other formats: Feeds:
#!/usr/bin/env python3
from argparse import ArgumentParser
from difflib import unified_diff
from subprocess import run
from sys import stderr
from tempfile import NamedTemporaryFile
from time import sleep
from urllib.request import urlopen
from xml.etree import ElementTree as ET
API = 'https://api.openstreetmap.org/api/0.6'
def clear_progress():
stderr.write('\n')
stderr.flush()
def fetch_xml(url, what, n, total, delay=200, spins=r'/-\|'):
sleep(delay / 1000)
stderr.write('\r{} {} {} out of {}'.format(spins[n % len(spins)], what, n, total))
stderr.flush()
with urlopen(url) as response:
tree = ET.parse(response)
return tree.getroot()
def parse_item_xml(kind, el):
data = {
'attrs': el.attrib,
'tags': {
tag.get('k'): tag.get('v')
for tag in el.findall('tag')
}
}
if kind == 'way':
data['refs'] = [
(ref.tag, ref.get('ref'), None)
for ref in el.findall('*[@ref]')
]
elif kind == 'relation':
data['refs'] = [
(member.get('type'), member.get('ref'), member.get('role'))
for member in el.findall('member')
]
return data
def bootstrap(csid):
items = {'node': {}, 'way': {}, 'relation': {}}
# changeset metadata mostly
xml = fetch_xml('{}/changeset/{}'.format(API, csid), 'changeset data', 1, 2, delay=0)
changeset = {
'attrs': xml.find('changeset').attrib,
'tags': {
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)
for node in xml:
if node.tag not in ('create', 'modify', 'delete'):
continue
el = node[0]
if el.tag not in ('node', 'way', 'relation'):
continue
id = int(el.get('id'))
version = int(el.get('version'))
items[el.tag][id] = {
version: parse_item_xml(el.tag, el)
}
# fetch previous versions
for kind in ('node', 'way', 'relation'):
clear_progress()
total = len(items[kind])
for n, id in enumerate(items[kind], 1):
version = min(items[kind][id])
if version == 1:
continue
version -= 1
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 = []
b = []
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]
old = min(versions)
new = max(versions)
todo = [(b, new)]
if new > 1:
todo.insert(0, (a, old))
for lines, version in todo:
if versions[version]['attrs']['visible'] != 'true':
continue
lines.append('')
lines.append('{} {}'.format(kind, id))
lines += [
' {} = {}'.format(k, v)
for k, v in sorted(versions[version]['tags'].items())
]
lines += [
' {} {}{}'.format(refmap.get(kind, kind), id, ' ' + role if role is not None else '')
for kind, id, role in versions[version].get('refs', [])
]
return a, b
def main():
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)
clear_progress()
if args.tool is None:
context = args.unified if args.unified is not None else 3
diff = unified_diff(a, b, n=context, lineterm='')
print('\n'.join(diff))
else:
with NamedTemporaryFile() as afile, NamedTemporaryFile() as bfile:
for fd, lines in ((afile, a), (bfile, b)):
fd.write('\n'.join(lines).encode('utf-8'))
fd.flush()
run([args.tool, afile.name, bfile.name])
if __name__ == '__main__':
main()