Skip to content

Commit 7464106

Browse files
committed
initial commit
0 parents  commit 7464106

File tree

6 files changed

+675
-0
lines changed

6 files changed

+675
-0
lines changed

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM ubuntu:latest
2+
MAINTAINER Michael Schwarz
3+
4+
COPY server.py client.py requirements.txt /opt/
5+
WORKDIR /opt
6+
7+
RUN apt-get update && \
8+
apt-get install -y python3 python3-pip git && \
9+
pip3 install flask python-Levenshtein bibtexparser GitPython
10+
11+
ENTRYPOINT ["python3", "server.py", "/data/", "main.bib"]
12+
13+
EXPOSE 5000
14+

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# BibTool
2+
3+
A tool to manage bibliography when collaboratively working on a LaTeX paper.
4+
5+

client.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import re
2+
import sys
3+
import requests
4+
import json
5+
import hashlib
6+
import bibtexparser
7+
from bibtexparser.bparser import BibTexParser
8+
import difflib
9+
import os
10+
import argparse
11+
12+
version = 9
13+
14+
limit_traffic = True
15+
16+
parser = argparse.ArgumentParser(description='BibTool')
17+
parser.add_argument("--token", dest="token", action="store", default="", help="Provide access token via command line")
18+
parser.add_argument("--tokenfile", dest="token_file", action="store", default="token", help="File containing the access token")
19+
parser.add_argument("--server", dest="server", action="store", default="", required=True, help="BibTool server")
20+
parser.add_argument("--tex", dest="tex", action="store", default="main.tex", help="BibTool server")
21+
parser.add_argument("--query", dest="query", action="store", default="", help="Query to search for (if action is search)")
22+
parser.add_argument("action")
23+
24+
args = parser.parse_args(sys.argv[1:])
25+
26+
if args.token != "":
27+
token = args.token
28+
else:
29+
token = None
30+
try:
31+
token = open(args.token_file).read().strip()
32+
except:
33+
pass
34+
35+
fname = args.tex
36+
server = args.server
37+
if server[-1] != '/': server += "/"
38+
if not server.endswith("/v1/"): server += "/v1/"
39+
40+
def get_keys():
41+
content = open(args.tex).read()
42+
keys = set()
43+
cites = re.findall("\\\\citeA?\\{([^\\}]+)\\}", content)
44+
for key in cites:
45+
keys |= set(key.split(","))
46+
47+
keys = sorted(list([k.strip() for k in keys]))
48+
return keys
49+
50+
51+
def keys_have_changed(keys):
52+
new_keys = hashlib.sha256("\n".join(keys).encode("utf-8")).hexdigest()
53+
old_keys = ""
54+
try:
55+
old_keys = open("main.bib.keys.sha").read().strip()
56+
except:
57+
pass
58+
try:
59+
open("main.bib.keys.sha", "w").write(new_keys)
60+
except:
61+
pass
62+
return (new_keys != old_keys)
63+
64+
65+
def bib_has_changed(bib):
66+
new_bib = hashlib.sha256(bib.strip().encode("utf-8")).hexdigest()
67+
old_bib = ""
68+
try:
69+
old_bib = open("main.bib.sha").read().strip()
70+
except:
71+
pass
72+
save_bib_hash()
73+
return (new_bib != old_bib)
74+
75+
76+
def entry_by_key(key):
77+
for entry in bib_database.entries:
78+
if entry["ID"] == key:
79+
return entry
80+
return None
81+
82+
83+
def entry_to_bibtex(entry):
84+
newdb = bibtexparser.bibdatabase.BibDatabase()
85+
newdb.entries = [ entry ]
86+
return bibtexparser.dumps(newdb)
87+
88+
89+
def inline_diff(a, b):
90+
matcher = difflib.SequenceMatcher(None, a, b)
91+
def process_tag(tag, i1, i2, j1, j2):
92+
if tag == 'replace':
93+
return '\u001b[34m[' + matcher.a[i1:i2] + ' -> ' + matcher.b[j1:j2] + ']\u001b[0m'
94+
if tag == 'delete':
95+
return '\u001b[31m[- ' + matcher.a[i1:i2] + ']\u001b[0m'
96+
if tag == 'equal':
97+
return matcher.a[i1:i2]
98+
if tag == 'insert':
99+
return '\u001b[32m[+ ' + matcher.b[j1:j2] + ']\u001b[0m'
100+
return ''.join(process_tag(*t) for t in matcher.get_opcodes())
101+
102+
103+
def resolve_changes():
104+
print("Your options are")
105+
print(" update server version with local changes (L)")
106+
print(" replace local version with server version (S)")
107+
print(" ignore, do not apply any changes (I)")
108+
print(" abort without changes (A)")
109+
while True:
110+
action = input("Your choice [l/s/I/a]: ").lower()
111+
if action == "l" or action == "s" or action == "a" or action == "i":
112+
return action
113+
if not action or action == "":
114+
return "i"
115+
return None
116+
117+
118+
def resolve_duplicate():
119+
print("Your options are")
120+
print(" commit local changes to server (M)")
121+
print(" delete server entry (D)")
122+
print(" remove local entry (R)")
123+
print(" ignore, do not apply any changes (I)")
124+
print(" abort without changes (A)")
125+
while True:
126+
action = input("Your choice [m/d/r/I/a]: ").lower()
127+
if action == "m" or action == "a" or action == "i" or action == "d" or action == "r":
128+
return action
129+
if not action or action == "":
130+
return "i"
131+
return None
132+
133+
134+
def update_local_bib(key, new_entry):
135+
for (idx, entry) in enumerate(bib_database.entries):
136+
if entry["ID"] == key:
137+
bib_database.entries[idx] = new_entry
138+
break
139+
140+
141+
def update_remote_bib(key, new_entry):
142+
response = requests.put(server + "entry/%s" % key, json = {"entry": new_entry, "token": token})
143+
if "success" in response.json() and not response.json()["success"]:
144+
show_error(response.json())
145+
146+
def add_remote_bib(key, entry):
147+
response = requests.post(server + "entry/%s" % key, json = {"entry": entry, "token": token})
148+
if "success" in response.json() and not response.json()["success"]:
149+
show_error(response.json())
150+
151+
def remove_remote_bib(key):
152+
response = requests.delete(server + "entry/%s%s" % (key, "/%s" % token if token else ""))
153+
if "success" in response.json() and not response.json()["success"]:
154+
show_error(response.json())
155+
156+
157+
def remove_local_bib(key):
158+
for (idx, entry) in enumerate(bib_database.entries):
159+
if entry["ID"] == key:
160+
del bib_database.entries[idx]
161+
save_bib()
162+
163+
164+
def save_bib_hash():
165+
try:
166+
bib = open("main.bib").read()
167+
open("main.bib.sha", "w").write(hashlib.sha256(bib.strip().encode("utf-8")).hexdigest())
168+
except:
169+
pass
170+
171+
172+
def save_bib():
173+
with open('main.bib', 'w') as bibtex_file:
174+
bibtexparser.dump(bib_database, bibtex_file)
175+
save_bib_hash()
176+
177+
178+
def show_error(obj):
179+
if "reason" in obj:
180+
if obj["reason"] == "access_denied":
181+
print("[!] Access denied! Your token is not valid for this operation. Verify whether the file '%s' contains a valid token." % args.token_file)
182+
183+
184+
action = args.action
185+
186+
parser = BibTexParser(common_strings=True)
187+
parser.ignore_nonstandard_types = False
188+
parser.homogenize_fields = True
189+
190+
if not os.path.exists("main.bib") or os.stat("main.bib").st_size == 0:
191+
bib_database = bibtexparser.loads("\n")
192+
else:
193+
try:
194+
with open('main.bib') as bibtex_file:
195+
bib_database = bibtexparser.load(bibtex_file, parser)
196+
#print(bib_database.entries)
197+
except Exception as e:
198+
print("Malformed bibliography file!\n")
199+
print(e)
200+
sys.exit(1)
201+
202+
response = requests.get(server + "version")
203+
version_info = response.json()
204+
if version_info["version"] > version:
205+
print("[!] New version available, updating...")
206+
script = requests.get(server + version_info["url"])
207+
with open(sys.argv[0], "w") as sc:
208+
sc.write(script.text)
209+
print("Restarting...")
210+
os.execl(sys.executable, *([sys.executable]+sys.argv))
211+
212+
213+
if action == "search":
214+
if len(args.query) < 3:
215+
print("Usage: %s search --query <query>" % sys.argv[0])
216+
sys.exit(1)
217+
response = requests.get(server + "search/" + args.query + ("/%s" % token if token else ""))
218+
print(response.text)
219+
220+
elif action == "sync":
221+
response = requests.get(server + "sync")
222+
print(response.text)
223+
224+
elif action == "get":
225+
keys = get_keys()
226+
fetch = keys_have_changed(keys)
227+
try:
228+
current_bib = open("main.bib").read()
229+
update = bib_has_changed(current_bib)
230+
except:
231+
update = False
232+
fetch = True
233+
234+
if update:
235+
fetch = True
236+
if not limit_traffic:
237+
update = True
238+
fetch = True
239+
#print("fetch %d, update %d\n" % (fetch, update))
240+
241+
# update
242+
if update:
243+
response = requests.post(server + "update", json = {"entries": bib_database.entries, "token": token})
244+
result = response.json()
245+
if not result["success"]:
246+
if result["reason"] == "duplicate":
247+
#print(result["entries"])
248+
for dup in result["entries"]:
249+
print("\n[!] There is already a similar entry for %s on the server (%s) [Levenshtein %d]" % (dup[1], dup[2]["ID"], dup[0]))
250+
print("- Local -")
251+
local = entry_to_bibtex(entry_by_key(dup[1]))
252+
remote = entry_to_bibtex(dup[2])
253+
print(local)
254+
print("- Server -")
255+
print(remote)
256+
print("- Diff - ")
257+
print(inline_diff(remote, local))
258+
259+
if dup[1] != dup[2]["ID"]:
260+
# different key, similar entry
261+
action = resolve_duplicate()
262+
if action == "i":
263+
pass
264+
elif action == "a":
265+
sys.exit(1)
266+
elif action == "d":
267+
remove_remote_bib(dup[2]["ID"])
268+
elif action == "m":
269+
add_remote_bib(dup[1], entry_by_key(dup[1]))
270+
elif action == "r":
271+
remove_local_bib(dup[1])
272+
else:
273+
# same key
274+
action = resolve_changes()
275+
if action == "a":
276+
sys.exit(1)
277+
elif action == "i":
278+
pass
279+
elif action == "s":
280+
update_local_bib(dup[1], dup[2])
281+
save_bib()
282+
elif action == "l":
283+
update_remote_bib(dup[2]["ID"], entry_by_key(dup[1]))
284+
else:
285+
show_error(result)
286+
287+
if fetch:
288+
response = requests.post(server + "get_json", json = {"entries": keys, "token": token})
289+
bib = response.json()
290+
if "success" in bib and not bib["success"]:
291+
show_error(bib)
292+
else:
293+
# merge local and remote database
294+
for entry in bib:
295+
if entry and "ID" in entry and not entry_by_key(entry["ID"]):
296+
bib_database.entries.append(entry)
297+
save_bib()
298+
299+
# suggest keys for unresolved keys
300+
for key in keys:
301+
if not entry_by_key(key):
302+
response = requests.get(server + "suggest/" + key + ("/%s" % (token if token else "")))
303+
suggest = response.json()
304+
if "success" in suggest and not suggest["success"]:
305+
show_error(suggest)
306+
else:
307+
print("Key '%s' not found%s %s" % (key, ", did you mean any of these?" if len(suggest["entries"]) > 0 else "", ", ".join(["'%s'" % e[1]["ID"] for e in suggest["entries"]])))
308+
309+
else:
310+
print("Unknown action '%s'" % action)

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bibtexparser
2+
requests
3+
argparse

requirements_server.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
flask
2+
python-Levenshtein
3+
bibtexparser
4+
GitPython

0 commit comments

Comments
 (0)