diff --git a/backend/commands.py b/backend/commands.py index e88b897..e8a4e02 100644 --- a/backend/commands.py +++ b/backend/commands.py @@ -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 diff --git a/backend/config.py b/backend/config.py index 3c78f6c..f28329a 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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") diff --git a/bmc.py b/bmc.py index e3836b6..d996377 100755 --- a/bmc.py +++ b/bmc.py @@ -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__':