Further refactor

This commit is contained in:
Lucas Verney 2016-03-30 19:14:18 +02:00
parent 691e752081
commit d6b75cacdd
3 changed files with 272 additions and 27 deletions

View File

@ -1,15 +1,48 @@
import libbmc
import os
import subprocess
import tempfile
from backend import config
from backend import tools
from libbmc import bibtex
from libbmc import fetcher
from libbmc.repositories import arxiv
from libbmc.papers import identifiers
from libbmc.papers import tearpages
def download(url, filetype, manual, autoconfirm, tag):
def get_entry_from_index(item, file_or_id=None):
"""
Fetch an entry from the global index.
:param item: An identifier or filename.
:param file_or_id: Whether it is a file or an entry identifier. If \
``None``, will try to match both.
:returns: TODO.
"""
entry = None
# If explictly an identifier
if file_or_id == "id":
entry = bibtex.get_entry(config.get("index"), item)
# If explicitely a filename
elif file_or_id == "file":
entry = bibtex.get_entry_by_filter(config.get("index"),
lambda x: x.file == item) # TODO
# Else, get by id or file
else:
entry = bibtex.get_entry_by_filter(config.get("index"),
lambda x: (
x.id == item or
x.file == item)) # TODO
return entry
def download(url, manual, autoconfirm, tag):
"""
Download a given URL and add it to the library.
:param url: URL to download.
:param filetype: paper / book / ``None``.
:param manual: Whether BibTeX should be fetched automatically.
:param autoconfirm: Whether import should be made silent or not.
:param tag: A tag for this file.
@ -30,7 +63,7 @@ def download(url, filetype, manual, autoconfirm, tag):
fh.write(dl)
# And add it as a normal paper from now on
new_name = import_file(tmp.name, filetype, manual,
new_name = import_file(tmp.name, manual,
autoconfirm, tag)
if new_name is None:
return None
@ -43,17 +76,200 @@ def download(url, filetype, manual, autoconfirm, tag):
return None
def import_file(src, filetype, manual, autoconfirm, tag, rename=True):
def import_file(src, manual=False, autoconfirm=False,
tag='', rename=True):
"""
Add a file to the library.
:param src: The path of the file to import.
:param filetype: paper / book / ``None``.
:param manual: Whether BibTeX should be fetched automatically.
:param autoconfirm: Whether import should be made silent or not.
:param tag: A tag for this file.
:param rename: TODO
:param manual: Whether BibTeX should be fetched automatically. \
Default to ``False``.
:param autoconfirm: Whether import should be made silent or not. \
Default to ``False``.
:param tag: A tag for this file. \
Default to no tag.
:param rename: Whether or not the file should be renamed according to the \
mask in the config.
:returns: The name of the imported file, or ``None`` in case of error.
"""
if not manual:
type, identifier = identifiers.find_identifiers(src)
if type is None:
tools.warning("Could not find an identifier for %s. \
Switching to manual entry." % (src))
# Fetch available identifiers types from libbmc
# Append "manual" for manual entry of BibTeX and "skip" to skip the
# file.
available_types_list = (libbmc.__valid_identifiers__ +
["manual", "skip"])
available_types = " / ".joint(available_types_list)
# Query for the type to use
while type not in available_types_list:
type = input("%s? " % (available_types)).lower()
if type == "skip":
# If "skip" is chosen, skip this file
return None
elif type == "manual":
identifier = None
else:
# Query for the identifier if required
identifier = input("Value? ")
else:
print("%s found for %s: %s." % (type, src, identifier))
# Fetch BibTeX automatically if we have an identifier
bibtex = None
if identifier is not None:
# If an identifier was provided, try to automatically fetch the bibtex
bibtex = identifiers.get_bibtex((type, identifier))
# TODO: Check bibtex
# Handle tag
if not autoconfirm:
# If autoconfirm is not enabled, query for a tag
user_tag = input("Tag for this paper [%s]? " % tag)
if user_tag != "":
tag = user_tag
bibtex["tag"] = tag
# TODO: Handle renaming
new_name = src
if rename:
pass
bibtex['file'] = os.path.abspath(new_name)
# Tear some pages if needed
should_tear_pages = True
if not autoconfirm:
# Ask for confirmation
pages_to_tear = tearpages.tearpage_needed(bibtex)
user_tear_pages = input("Found some pages to tear: %s. \
Confirm? [Y/n]" % (pages_to_tear)).lower()
if user_tear_pages == "n":
should_tear_pages = False
if should_tear_pages:
tearpages.tearpage(new_name, bibtex=bibtex)
# TODO: Append to global bibtex index
return new_name
def delete(item, keep=False, file_or_id=None):
"""
Delete an entry in the main BibTeX file, and the associated documents.
:param item: An entry or filename to delete from the database.
:param keep: Whether or not the document should be kept on the disk. \
If True, will simply delete the entry in the main BibTeX index.
:param file_or_id: Whether it is a file or an entry identifier. If \
``None``, will try to match both.
:returns: Nothing.
"""
entry = get_entry_from_index(item, file_or_id)
# Delete the entry from the bibtex index
bibtex.delete(config.get("index"), entry.id) # TODO
# If file should not be kept
if not keep:
# Delete it
os.unlink(entry.file) # TODO
def edit(item, file_or_id):
"""
Edit an entry in the main BibTeX file.
:param item: An entry or filename to edit in the database.
:param file_or_id: Whether it is a file or an entry identifier. If \
``None``, will try to match both.
:returns: Nothing.
"""
# TODO
pass
def list_entries():
"""
List all the available entries and their associated files.
:returns: A dict with entry identifiers as keys and associated files as \
values.
"""
# Get the list of entries from the BibTeX index
entries_list = bibtex.get(config.get("index"))
return {entry.id: entry.file for entry in entries_list} # TODO
def open(id):
"""
Open the file associated with the provided entry identifier.
:param id: An entry identifier in the main BibTeX file.
:returns: ``False`` if an error occured. ``True`` otherwise.
"""
# Fetch the entry from the BibTeX index
entry = bibtex.get_entry(config.get("index"), id)
if entry is None:
return False
else:
# Run xdg-open on the associated file to open it
subprocess.Popen(['xdg-open', entry.filename]) # TODO
return True
def export(item, file_or_id=None):
"""
Export the BibTeX entries associated to some items.
:param item: An entry or filename to export as BibTeX.
:param file_or_id: Whether it is a file or an entry identifier. If \
``None``, will try to match both.
:returns: TODO.
"""
# Fetch the entry from the BibTeX index
entry = get_entry_from_index(item, file_or_id)
if entry is not None:
return bibtex.dict2BibTeX(entry) # TODO
def resync():
"""
Compute the diff between the main BibTeX index and the files on the disk,
and try to resync them.
:returns: Nothing.
"""
# TODO
pass
def update(item, file_or_id=None):
"""
Update an entry, trying to fetch a more recent version (on arXiv for \
instance.)
:param item: An entry or filename to fetch update from.
:param file_or_id: Whether it is a file or an entry identifier. If \
``None``, will try to match both.
:returns: TODO.
"""
entry = get_entry_from_index(item, file_or_id)
# Fetch latest version
latest_version = arxiv.get_latest_version(entry.eprint) # TODO
if latest_version != entry.eprint: # TODO
print("New version found for %s: %s" % (entry, latest_version))
confirm = input("Download it? [Y/n] ")
if confirm.lower() == 'n':
return
# Download the updated version
# TODO
# Delete previous version if needed
# TODO

View File

@ -55,6 +55,7 @@ class Config():
def initialize(self):
self.set("folder", os.path.expanduser("~/Papers/"))
self.set("index", os.path.expanduser("~/Papers/index.bib"))
self.set("proxies", [''])
self.set("format_articles", "%f_%l-%j-%Y%v")
self.set("format_books", "%a-%t")

64
bmc.py
View File

@ -15,6 +15,14 @@ from backend.config import Config
EDITOR = os.environ.get("EDITOR")
def file_or_id_from_args(args):
"""
Helper function to parse provided args to check if the argument is a \
file or an identifier.
"""
return "id" if args.id else "file" if args.file else None
def parse_args():
"""
Build a parser and parse arguments of command line.
@ -25,13 +33,10 @@ def parse_args():
description="A bibliography management tool.")
parser.add_argument("-c", "--config", default=None,
help="path to a custom config dir.")
subparsers = parser.add_subparsers(help="sub-command help", dest='parser')
subparsers = parser.add_subparsers(help="sub-command help", dest='command')
subparsers.required = True # Fix for Python 3.3.5
parser_download = subparsers.add_parser('download', help="download help")
parser_download.add_argument('-t', '--type', default=None,
choices=['paper', 'book'],
help="type of the file to download")
parser_download.add_argument('-m', '--manual', default=False,
action='store_true',
help="disable auto-download of bibtex")
@ -46,9 +51,6 @@ def parse_args():
parser_download.set_defaults(func='download')
parser_import = subparsers.add_parser('import', help="import help")
parser_import.add_argument('-t', '--type', default=None,
choices=['paper', 'book'],
help="type of the file to import")
parser_import.add_argument('-m', '--manual', default=False,
action='store_true',
help="disable auto-download of bibtex")
@ -100,16 +102,30 @@ def parse_args():
parser_open.set_defaults(func='open')
parser_export = subparsers.add_parser('export', help="export help")
parser_export.add_argument('ids', metavar='id', nargs='+',
help="an identifier")
parser_export.add_argument('entries', metavar='entry', nargs='+',
help="a filename or an identifier")
parser_export.add_argument('--skip', nargs='+',
help="path to files to skip", default=[])
group = parser_export.add_mutually_exclusive_group()
group.add_argument('--id', action="store_true", default=False,
help="id based deletion")
group.add_argument('--file', action="store_true", default=False,
help="file based deletion")
parser_export.set_defaults(func='export')
parser_resync = subparsers.add_parser('resync', help="resync help")
parser_resync.set_defaults(func='resync')
parser_update = subparsers.add_parser('update', help="update help")
parser_update.add_argument('--entries', metavar='entry', nargs='+',
parser_update.add_argument('entries', metavar='entry', nargs='+',
help="a filename or an identifier")
parser_update.add_argument('--skip', nargs='+',
help="path to files to skip", default=[])
group = parser_update.add_mutually_exclusive_group()
group.add_argument('--id', action="store_true", default=False,
help="id based deletion")
group.add_argument('--file', action="store_true", default=False,
help="file based deletion")
parser_update.set_defaults(func='update')
return parser.parse_args()
@ -133,7 +149,7 @@ def main():
skipped = []
for url in args.url:
# Try to download the URL
new_name = commands.download(url, args.type, args.manual, args.y,
new_name = commands.download(url, args.manual, args.y,
args.tag)
if new_name is not None:
print("%s successfully imported as %s." % (url, new_name))
@ -154,7 +170,7 @@ def main():
files_to_process = list(set(args.file) - set(args.skip))
for filename in files_to_process:
# Try to import the file
new_name = commands.import_file(filename, args.type,
new_name = commands.import_file(filename,
args.manual, args.y,
args.tag, not args.inplace)
if new_name is not None:
@ -184,7 +200,7 @@ def main():
# Try to delete the item
if confirm.lower() == 'y':
file_or_id = "id" if args.id else "file" if args.file else None
file_or_id = file_or_id_from_args(args)
commands.delete(item, args.keep, file_or_id)
print("%s successfully deleted." % (item,))
else:
@ -200,13 +216,13 @@ def main():
# Handle exclusions
items_to_process = list(set(args.entries) - set(args.skip))
for item in items_to_process:
file_or_id = "id" if args.id else "file" if args.file else None
file_or_id = file_or_id_from_args(args)
commands.edit(item, file_or_id)
# List command
elif args.func == 'list':
# List all available items
for id, file in commands.list().items():
for id, file in commands.list_entries().items():
# And print them as "identifier: file"
print("%s: %s" % (id, file))
@ -214,14 +230,18 @@ def main():
elif args.func == 'open':
# Open each entry
for id in args.ids:
if commands.open(id) is None:
if not commands.open(id):
# And warn the user about missing files or errors
tools.warning("Unable to open file associated with ident %s." %
(id,))
# Export command
elif args.func == 'export':
print(commands.export(args.ids))
# Handle exclusions
items_to_process = list(set(args.entries) - set(args.skip))
for item in items_to_process:
file_or_id = file_or_id_from_args(args)
print(commands.export(item, file_or_id))
# Resync command
elif args.func == 'resync':
@ -231,7 +251,15 @@ def main():
# Update command
elif args.func == 'update':
commands.update(args.entries)
# Handle exclusions
items_to_process = list(set(args.entries) - set(args.skip))
for item in items_to_process:
file_or_id = file_or_id_from_args(args)
updates = commands.update(args.entries)
# TODO \/
print("%d new versions of papers were found:" % (len(updates)))
for item in updates:
print(item)
if __name__ == '__main__':