# -*- coding: utf-8 -*-
################################################################################
# #
# This file is part of the Bibolamazi Project. #
# Copyright (C) 2018 by Philippe Faist #
# philippe.faist@bluewin.ch #
# #
# Bibolamazi is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# Bibolamazi is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with Bibolamazi. If not, see <http://www.gnu.org/licenses/>. #
# #
################################################################################
"""
This module contains the code that implements Bibolamazi's main functionality. It also
provides the basic tools for the command-line interface.
"""
import os
import os.path
import re
import sys
import argparse
import textwrap
from collections import namedtuple
import json
import logging
import appdirs
# import all the parts we need from our own application.
# ------------------------------------------------------
import bibolamazi.init
# rest of the modules
from . import blogger
from . import version
from .bibolamazifile import BibolamaziFile
from . import argparseactions
from . import butils
from .butils import BibolamaziError
from .bibfilter import factory as filterfactory
from .bibfilter import pkgprovider, pkgfetcher_github
# our logger for the main module
logger = logging.getLogger(__name__)
from .bibfilter.argtypes import LogLevel
# ------------------------------------------------------------------------------
# code to set up logging mechanism, if run by command-line
[docs]def verbosity_logger_level(verbosity):
"""
Simple mapping of 'verbosity level' (used, for example for command line
options) to correspondig logging level (:py:const:`logging.DEBUG`,
:py:const:`logging.ERROR`, etc.).
"""
if verbosity == 0:
return logging.ERROR
elif verbosity == 1:
return logging.INFO
elif verbosity == 2:
return logging.DEBUG
elif verbosity >= 3:
return blogger.LONGDEBUG
raise ValueError("Bad verbosity level: %r" %(verbosity))
# ------------------------------------------------------------------------------
[docs]class BibolamaziNoSourceEntriesError(BibolamaziError):
def __init__(self):
msg = "Error: No source entries found. Stopping before we overwrite the bibolamazi file."
BibolamaziError.__init__(self, msg)
[docs]def setup_filterpackage_from_argstr(argstr):
"""
Add a filter package definition and path to filterfactory.filterpath from a string
that is a e.g. a command-line argument to --filterpackage or a part of the environment
variable BIBOLAMAZI_FILTER_PATH.
"""
(fpname, fpdir) = filterfactory.parse_filterpackage_argstr(argstr)
try:
ok = filterfactory.validate_filter_package(fpname, fpdir, raise_exception=True)
except filterfactory.NoSuchFilterPackage as e:
raise BibolamaziError(str(e))
filterfactory.filterpath[fpname] = fpdir
[docs]def setup_filterpackages_from_env():
if 'BIBOLAMAZI_FILTER_PATH' in os.environ:
logger.debug("Detected BIBOLAMAZI_FILTER_PATH=%s, using it" %(os.environ['BIBOLAMAZI_FILTER_PATH']))
for fp in reversed(os.environ['BIBOLAMAZI_FILTER_PATH'].split(os.pathsep)):
setup_filterpackage_from_argstr(fp)
[docs]class AddFilterPackageAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setup_filterpackage_from_argstr(values)
[docs]def get_args_parser():
parser = argparse.ArgumentParser(
description='Prepare consistent BibTeX files for your LaTeX documents',
prog='bibolamazi',
epilog="Log messages will be produced in color by default "
"if outputting to a TTY. To override the use of TTY colors, "
"set environment variable BIBOLAMAZI_TTY_COLORS to 'yes', 'no' "
"or 'auto'.",
add_help=False)
group = parser.add_argument_group("Bibolamazi file")
group.add_argument(
'-o', '--output', action='store', dest='output', metavar="FILE", nargs='?',
help="Do not overwrite the original bibolamazi file, and write "
"instead bibolamazi output to FILE. (Note: the cache is still "
"saved using the old file name with extension \".bibolamazicache\" "
"for future use.)"
)
group.add_argument(
'-N', '--new', action=argparseactions.opt_init_empty_template, nargs=1,
metavar="NEW_FILENAME",
help="Create a new bibolamazi file with a template configuration."
)
group = parser.add_argument_group("Cache control")
group.add_argument(
'-C', '--no-cache', action='store_false', dest='use_cache', default=True,
help="Do not read any existing cache file and regenerate the cache. If "
"the cache file exists, it will be overwritten."
)
group.add_argument(
'-z', '--cache-timeout', dest='cache_timeout', type=butils.parse_timedelta,
default=None,
help="The default timeout after which to consider items in cache to be invalid. "
"Not all cache items honor this. Format: '<N><unit>' with unit=w/d/m/s"
)
group = parser.add_argument_group("Filter packages")
group.add_argument(
'--filterpackage', action=AddFilterPackageAction,
help="Add a package name in which to search for filters. You may specify this "
"option multiple times; last specified filter packages are searched first. Valid "
"values for this option are either (1) a simple python package name (if it is in the "
"PYTHONPATH); (2) a string 'pkgname=/some/location' where pkgname is the python "
"package name which will be loaded with the given path prepended to sys.path; or "
"(3) a full path to a package directory '/some/location/to/pkgname' which has the "
"same effect as the value 'pkgname=/some/location/to'."
)
group.add_argument(
'--github-auth', action=argparseactions.opt_action_github_auth, nargs='?',
help="Store authentication information for accessing filter packages specified "
"directly as github repositories. Use this option without argument for an "
"interactive setup, or if you know what you're doing, directly specify the "
"access token as argument to this option or specify '-' as argument to reset "
"the stored authentication."
)
group = parser.add_argument_group("Logging verbosity")
group.add_argument(
'--verbosity', action=argparseactions.opt_set_verbosity, nargs=1,
help="Set verbosity level (0=quiet, 1=info (default), 2=verbose, 3=long debug)."
)
group.add_argument(
'-q', '-v0', '--quiet', action=argparseactions.opt_set_verbosity, nargs=0, const=0,
help="Don't display any messages (same as --verbosity=0)"
)
group.add_argument(
'-v1', action=argparseactions.opt_set_verbosity, nargs=0, const=1,
help='Set normal verbosity mode (same as --verbosity=1)'
)
group.add_argument(
'-v', '-v2', '--verbose', action=argparseactions.opt_set_verbosity, nargs=0, const=2,
help='Set verbose mode (same as --verbosity=2)'
)
group.add_argument(
'-vv', '-v3', '--long-verbose', action=argparseactions.opt_set_verbosity,
nargs=0, const=3,
help='Set very verbose mode, with long debug messages (same as --verbosity=3)'
)
group.add_argument(
'--fine-log-levels', action=argparseactions.opt_set_fine_log_levels,
help=textwrap.dedent('''\
Fine-grained logger control: useful for debugging filters or
bibolamazi itself. This is a comma-separated list of modules and
corresponding log levels to set, e.g.
"core=INFO,filters=DEBUG,filters.arxiv=LONGDEBUG", where if in an
item no module is given (but just a level or number), then the
root logger is addressed. Possible levels are (%s)
''')%(
", ".join( (x[0] for x in LogLevel.levelnos) )
)
)
group = parser.add_argument_group("Help pages")
group.add_argument(
'--help', '-h', action=argparseactions.opt_action_help, nargs='?',
metavar='filter',
help='Show this help message and exit. If filter is given, show information and '
'help text for that filter. See --list-filters for a list of available filters.'
)
group.add_argument(
'--help-welcome', action=argparseactions.opt_action_helpwelcome, nargs=0,
help='Show a brief introduction to bibolamazi and how to use it.'
)
group.add_argument(
'-F', '--list-filters', action=argparseactions.opt_list_filters, dest='list_filters',
help="Show a list of available filters along with their description, and exit."
)
group.add_argument(
'--version', action=argparseactions.opt_action_version, nargs=0,
help='Show bibolamazi version number and exit.'
)
parser.add_argument(
'bibolamazifile',
# note the %'s are parsed as formatting:
help='The .bibolamazi.bib file to update, i.e. that contains the %%%%%%-BIB-OLA-MAZI '
'configuration tags.'
)
return parser
ArgsStruct = namedtuple('ArgsStruct', ('bibolamazifile', 'use_cache', 'cache_timeout', 'output'))
[docs]def main(argv=sys.argv[1:]):
try:
# run main program
_main_helper(argv)
except SystemExit:
raise
except KeyboardInterrupt:
raise
except BibolamaziError as e:
logger.error("\n" + str(e))
except: # lgtm [py/catch-base-exception]
print()
print(" -- EXCEPTION --")
print()
# debugging post-mortem
import traceback; traceback.print_exc()
import pdb; pdb.post_mortem()
[docs]class CmdlSettings:
"""
Stores settings for the command-line app. Read/write json-objects to the
`config` property of this object. Config is loaded upon object
creation. Call `saveConfig()` after changing the `config` property.
"""
def __init__(self, configfname='cmdl_settings.json'):
super().__init__()
self.configfname = configfname
self.user_config_dir = appdirs.user_config_dir('bibolamazi')
if not os.path.exists(self.user_config_dir):
os.makedirs(self.user_config_dir, exist_ok=True)
self.full_config_fname = os.path.join(self.user_config_dir, self.configfname)
self.config = {}
self.reloadConfig()
if 'RemoteFilterPackages' not in self.config:
self.config['RemoteFilterPackages'] = {}
[docs] def reloadConfig(self):
if os.path.exists(self.full_config_fname):
try:
with open(self.full_config_fname) as f:
self.config = json.load(f)
except Exception as e:
logger.warning("Failed to load config file %s: %s", self.full_config_fname, e)
[docs] def saveConfig(self):
with open(self.full_config_fname, 'w') as f:
json.dump(self.config, f, indent=4)
# Note: not used by GUI
[docs]class CmdlMainPackageProviderManager(pkgprovider.PackageProviderManager):
def __init__(self):
super().__init__()
settings = CmdlSettings()
self.prompted_for_remote = settings.config['RemoteFilterPackages'].get('PromptedForRemote', False)
self.allow_remote = settings.config['RemoteFilterPackages'].get('AllowRemote', False)
[docs] def remoteAllowed(self):
if not self.prompted_for_remote:
# ask for remote
print("""\
WARNING: Filter packages are python scripts that can execute arbitrary code.
Only run filters from sources you trust. Do you want to enable automatically
downloading remote packages when a remote package is specified?
""")
yn = None
while yn not in ['Y', 'n']:
yn = input('Allow remote packages? (Y/n) ')
yn = yn.strip()[0]
if yn not in ['Y', 'n']:
print("Please answer with Y or n.")
self.allow_remote = ( yn == 'Y' )
self.prompted_for_remote = True
settings = CmdlSettings()
settings.config['RemoteFilterPackages']['PromptedForRemote'] = True
settings.config['RemoteFilterPackages']['AllowRemote'] = self.allow_remote
settings.saveConfig()
if self.allow_remote:
logger.warning("Allowing remote filter packages for future sessions. Edit "
"config file %s to change this.", settings.full_config_fname)
elif not self.allow_remote:
settings = CmdlSettings()
logger.warning("Remote filter packages have been disabled. Edit config file %s"
" to change.", settings.full_config_fname)
return self.allow_remote
# Note: gui doesn't use these, see bibolamazi_gui.bibolamaziapp
cmdl_filterpackage_providers = {}
[docs]def load_filterpackage_providers():
settings = CmdlSettings()
# first, create our package provider manager.
filterfactory.package_provider_manager = CmdlMainPackageProviderManager()
github_auth_token = settings.config['RemoteFilterPackages'].get('GithubAuthToken', '')
if github_auth_token:
github_auth_token = github_auth_token.strip()
if not github_auth_token:
github_auth_token = None
cmdl_filterpackage_providers['github'] = \
pkgfetcher_github.GithubPackageProvider(github_auth_token)
filterfactory.package_provider_manager.registerPackageProvider(
cmdl_filterpackage_providers['github']
)
#print("Loaded filterpackage providers (cmdl version)")
[docs]def get_github_auth_status():
"""
Returns one of `None` (no configuration provided), `False` (configuration
exists, token explicitly not set), and `True` (token previously set and
saved).
"""
settings = CmdlSettings()
if 'RemoteFilterPackages' not in settings.config:
return None
if 'GithubAuthToken' not in settings.config['RemoteFilterPackages']:
return None
token = settings.config['RemoteFilterPackages']['GithubAuthToken']
if token is not None and token.strip():
return True
return False
def _check_token_valid(token):
if re.match(r'^[a-zA-Z0-9.+_/!@*=-]{32,}$', token) is None:
raise ValueError("Invalid access token provided")
[docs]def save_github_auth_token(github_auth_token):
# no need to mess with objects in cmdl_filterpcakage_providers -- anyway
# we'll quit right away after saving the token to the config file.
#
#cmdl_filterpackage_providers['github'].setAuthToken(github_auth_token)
settings = CmdlSettings()
if 'RemoteFilterPackages' not in settings.config:
settings.config['RemoteFilterPackages'] = {}
if github_auth_token is not None:
_check_token_valid(github_auth_token) # raise ValueError for invalid token
settings.config['RemoteFilterPackages']['GithubAuthToken'] = github_auth_token
settings.saveConfig()
logger.debug("Set auth token %s",
'[...]{}'.format(github_auth_token[-4:]) if github_auth_token else 'None')
if github_auth_token is not None:
logger.info("Github authentication token set.")
else:
logger.info("Unset github authentication token.")
def _main_helper(argv):
# get some basic logging mechanism running
blogger.setup_simple_console_logging()
# start with level INFO
logging.getLogger().setLevel(logging.INFO)
# load precompiled filters, if we've got any
# ------------------------------------------
#try:
# import bibolamazi.bibolamazi_compiled_filter_list as pc
# filters_factory.load_precompiled_filters('bibolamazi.filters', dict([
# (fname, pc.__dict__[fname]) for fname in pc.filter_list
# ]))
#except ImportError:
# pass
# set up the filter package providers
# -----------------------------------
load_filterpackage_providers()
# set up extra filter packages from environment variables
# -------------------------------------------------------
setup_filterpackages_from_env()
# parse the command line arguments
# --------------------------------
parser = get_args_parser()
args = parser.parse_args(args=argv)
return run_bibolamazi_args(args)
[docs]def run_bibolamazi(bibolamazifile, **kwargs):
# defaults
kwargs2 = {
'use_cache': True,
'cache_timeout': None,
'output': None
}
kwargs2.update(kwargs)
args = ArgsStruct(bibolamazifile, **kwargs2)
return run_bibolamazi_args(args)
[docs]def run_bibolamazi_args(args):
#
# args is supposed to be the parsed arguments from main()
#
logger.debug(textwrap.dedent("""
Bibolamazi Version %(ver)s by Philippe Faist (C) %(copy_year)s
Use option --help for help information.
""" % {
'ver': version.version_str,
'copy_year': version.copyright_year,
}))
# open the bibolamazifile, which is the main bibtex file
# ------------------------------------------------------
kwargs = {
'use_cache': args.use_cache
}
#
# If given a cache_timeout, give it as parameter
#
if args.cache_timeout is not None:
logger.debug("default cache timeout: %r", args.cache_timeout)
kwargs['default_cache_invalidation_time'] = args.cache_timeout
# open the bibolamazi file and create the BibolamaziFile object. This will parse the rules
# and the entries, as well as keep some information on how to re-write to the file.
bfile = BibolamaziFile(args.bibolamazifile, **kwargs)
bibdata = bfile.bibliographyData()
if (bibdata is None or not len(bibdata.entries)):
logger.critical("No source entries found. Stopping before we overwrite the bibolamazi file.")
raise BibolamaziNoSourceEntriesError()
# now, run the selected filters in the corresponding order.
# ---------------------------------------------------------
for filtr in bfile.filters():
#
# For debugging: dump the library at each filter step on level longdebug()
#
if logger.isEnabledFor(blogger.LONGDEBUG):
s = "========== Dumping Bibliography Database ==========\n"
for key, entry in bibdata.entries.items():
s += " %10s: %r\n\n"%(key, entry)
s += "===================================================\n"
logger.longdebug(s)
bfile.runFilter(filtr)
# and output everything ...
if args.output:
# ... to the specified file:
bfile.saveToFile(fname=args.output)
else:
# ... or back to the original file:
bfile.saveToFile()
logger.debug('Done.')
return None
if __name__ == "__main__":
main()