├── zoterocleanup ├── __init__.py ├── _utils.py ├── _dois.py ├── _notes.py ├── _automerge.py ├── _zotero.py ├── _tags.py └── _authors.py ├── setup.py ├── README.md ├── .gitignore ├── bin └── zoterocleanup ├── ez_setup.py └── LICENSE /zoterocleanup/__init__.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | 3 | from ._authors import rename_author, merge_authors 4 | from ._automerge import auto_merge 5 | from ._dois import check_for_missing_dois 6 | from ._notes import remove_notes 7 | from ._tags import remove_stars, merge_duplicate_tags 8 | from ._zotero import connect 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | from ez_setup import use_setuptools 3 | use_setuptools() 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup(name='zoterocleanup', 8 | version='dev', 9 | description='Zotero library management tools', 10 | author='Christian Brodbeck', 11 | author_email='christianbrodbeck@nyu.edu', 12 | url='https://github.com/christianbrodbeck/zotero-cleanup', 13 | packages=['zoterocleanup'], 14 | scripts=['bin/zoterocleanup'], 15 | install_requires=['pyzotero', 'keyring >= 5.0', 'beautifulsoup4']) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zotero-cleanup 2 | 3 | Maintenance of a Zotero library based on 4 | [pyzotero](http://pyzotero.readthedocs.io). 5 | 6 | Installs the script: 7 | 8 | $ zoterocleanup 9 | 10 | These functions modify the library hosted on [zotero.org](http://zotero.org). 11 | Keep a local backup of your library! 12 | 13 | 14 | ## Normalizing author names 15 | 16 | If the same author has entries with different spellings in a library 17 | (e.g., "Albert Einstein" vs. "A Einstein") this can lead to errors in 18 | bibliographies with standards for disambiguating authors with similar names. 19 | This function scans the library for alternate spellings of the same author and 20 | changes them to a unified version: 21 | 22 | $ zoterocleanup merge-authors 23 | 24 | A list of all changes is printed for confirmation before applying any changes. 25 | -------------------------------------------------------------------------------- /zoterocleanup/_utils.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | 3 | 4 | def ask(prompt, options=('y', 'n'), default=None): 5 | """ 6 | Parameters 7 | ---------- 8 | prompt : str 9 | Prompt to ask the user (without options). 10 | options : tuple of str 11 | Options of valid responses. 12 | 13 | Returns 14 | ------- 15 | response : str 16 | User response, lowercase. 17 | """ 18 | prompt += " [%s] " % '/'.join(options) 19 | options_lower = [o.lower() for o in options] 20 | while True: 21 | choice = raw_input(prompt).lower() 22 | if default is not None and choice == '': 23 | return default 24 | elif choice in options_lower: 25 | return choice 26 | else: 27 | print("Invalid response.") 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # PyCharm 57 | /.idea 58 | -------------------------------------------------------------------------------- /zoterocleanup/_dois.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | import re 3 | import urllib 4 | from ._zotero import connect 5 | 6 | 7 | REQUEST = ('http://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/' 8 | '?tool=zoterocleanup&email=christianbrodbeck@nyu.edu&ids=') 9 | 10 | 11 | def find_doi(source, identifier): 12 | if source == 'PMID' or source == 'DOI': 13 | request = REQUEST + str(identifier) 14 | elif source == 'PMCID': 15 | request = REQUEST + 'PMC' + str(identifier) 16 | elif source == 'NIHMS': 17 | request = REQUEST + 'NIHMS' + str(identifier) 18 | else: 19 | raise ValueError("Unknown source: %r" % source) 20 | txt = urllib.urlopen(request).read() 21 | m = re.match('doi="([0-9a-zA-Z/.]+)"', txt) 22 | if m: 23 | return m.group(1) 24 | # else: 25 | # print "No DOI returned in:" + txt 26 | 27 | 28 | def check_for_missing_dois(library=None): 29 | z = connect(library) 30 | print("Retrieving Library...") 31 | items = z.everything(z.top()) 32 | 33 | print("Items missing DOI:") 34 | changed = [] 35 | for i, item in enumerate(items): 36 | if not item['data'].get('DOI'): 37 | if 'extra' in item['data']: 38 | m = re.match("PMID: (\d+)", item['data']['extra']) 39 | else: 40 | m = None 41 | 42 | if m: 43 | pmid = m.group(1) 44 | doi = find_doi('PMID', pmid) 45 | if doi: 46 | item['data']['DOI'] = doi 47 | print("%s -> %s" % (item['data']['key'], doi)) 48 | changed.append(i) 49 | else: 50 | m = None 51 | 52 | if m is None: 53 | print(item['data']['key']) 54 | -------------------------------------------------------------------------------- /zoterocleanup/_notes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from bs4 import BeautifulSoup 4 | 5 | from ._utils import ask 6 | from ._zotero import connect 7 | 8 | 9 | AUTO_TOC_START = 'Contents

' 10 | 11 | 12 | def remove_notes(library=None, verbose=True, pattern=AUTO_TOC_START): 13 | """Remove notes matching a pattern 14 | 15 | Parameters 16 | ---------- 17 | library : str 18 | Library ID. 19 | verbose : bool 20 | Print all the notes that would be removed. 21 | pattern : str 22 | The pattern that notes are matched with (the default is 23 | ``"

Contents

"``, the beginning of automatically 24 | generated table of contents). 25 | """ 26 | if pattern is None: 27 | pattern = AUTO_TOC_START 28 | z = connect(library) 29 | print("Retrieving Library...") 30 | items = z.everything(z.top()) 31 | print("Scanning children for notes...") 32 | changed_items = [] 33 | for item in items: 34 | children = z.children(item['key']) 35 | for child in children: 36 | if re.match(pattern, child['data']['note']): 37 | if verbose: 38 | title = child['data']['title'][:80] 39 | print(title) 40 | print('-' * len(title)) 41 | soup = BeautifulSoup(child['data']['note'], "lxml") 42 | print(soup.get_text() + '\n') 43 | child['data']['note'] = '' 44 | changed_items.append(child) 45 | 46 | if not changed_items: 47 | print("No notes with auto TOC found") 48 | return 49 | print("%i notes found. Should they be deleted?" % len(changed_items)) 50 | if ask("Proceed?") == 'y': 51 | print("Updating library...") 52 | for item in changed_items: 53 | z.update_item(item) 54 | -------------------------------------------------------------------------------- /zoterocleanup/_automerge.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | from collections import defaultdict 3 | 4 | from ._zotero import connect, date_added 5 | 6 | 7 | def auto_merge(library=None): 8 | """Merge duplicate items 9 | 10 | Parameters 11 | ---------- 12 | library : str | int 13 | Library ID. 14 | 15 | Notes 16 | ----- 17 | Find duplicate items by DOI and ISBN. For each identifier, Keep only the 18 | oldest item, but use the attachments from the newest item that has 19 | attachments. 20 | """ 21 | z = connect(library) 22 | 23 | print("Retrieving Library...") 24 | items = z.everything(z.top()) 25 | 26 | print("Resolving duplicates...") 27 | # sort items by DOI 28 | by_doi = defaultdict(list) 29 | for item in items: 30 | if 'DOI' in item['data']: 31 | by_doi[item['data']['DOI']].append(item) 32 | elif 'ISBN' in item['data']: 33 | by_doi[item['data']['ISBN']].append(item) 34 | 35 | delete = [] 36 | update = [] 37 | for doi, items in by_doi.iteritems(): 38 | if len(items) == 1: 39 | continue 40 | 41 | # sort by age 42 | items.sort(key=date_added) 43 | 44 | # keep oldest item 45 | keep = items[0] 46 | 47 | # keep latest attachments 48 | keep_cs = z.children(keep['key']) 49 | for item in items[-1:0:-1]: 50 | cs = z.children(item['key']) 51 | if cs: 52 | for c in cs: 53 | c['data']['parentItem'] = keep['key'] 54 | update.extend(cs) 55 | delete.extend(keep_cs) 56 | break 57 | 58 | delete.extend(items[1:]) 59 | 60 | print("Updating library...") 61 | # update first, so we don't delete parents of items we want to keep 62 | for item in update: 63 | z.update_item(item) 64 | for item in delete: 65 | z.delete_item(item) 66 | -------------------------------------------------------------------------------- /bin/zoterocleanup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Author: Christian Brodbeck 4 | import argparse 5 | from zoterocleanup import merge_authors, check_for_missing_dois, remove_notes 6 | from zoterocleanup._notes import AUTO_TOC_START 7 | 8 | 9 | # create the top-level parser 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument( 12 | '--library', default=None, 13 | help='Use a specific library. A valid library ID is a sequence of digits. ' 14 | 'During the first execution, zoterocleanup automatically asks for the ' 15 | 'library if it is not specified. After that, zoterocleanup remembers ' 16 | 'the library from the first exeution, but it can be changed with this ' 17 | 'argument.') 18 | subparsers = parser.add_subparsers(help='sub-command help') 19 | 20 | # author-names 21 | subparser = subparsers.add_parser( 22 | 'merge-authors', 23 | help='Find author names that seem to be different spellings of the same ' 24 | 'author. ') 25 | subparser.add_argument( 26 | '-v', '--verbose', 27 | action='store_true', 28 | help="Display more information") 29 | subparser.set_defaults( 30 | func=lambda a: merge_authors(a.library, a.verbose)) 31 | 32 | # journal abbreviations 33 | # subparser = subparsers.add_parser('abbr') 34 | # subparser.add_argument('command', help=" | journal-abbr") 35 | 36 | # DOIs 37 | subparser = subparsers.add_parser( 38 | 'doi', 39 | help='Check for missing DOIs') 40 | subparser.set_defaults( 41 | func=lambda a: check_for_missing_dois(a.library)) 42 | 43 | # notes 44 | subparser = subparsers.add_parser( 45 | 'remove-notes', 46 | help='Remove notes matchin a given pattern from attachments. The default ' 47 | 'is to remove auto-generated ') 48 | subparser.add_argument( 49 | '-q', '--quiet', 50 | dest='verbose', 51 | action='store_false', 52 | help="Don't print the content of the matching notes") 53 | subparser.add_argument( 54 | '-p', '--pattern', 55 | default=AUTO_TOC_START, 56 | help="Pattern to match notes") 57 | subparser.set_defaults( 58 | func=lambda a: remove_notes(a.library, a.verbose, a.pattern)) 59 | 60 | # parse and execute 61 | args = parser.parse_args() 62 | args.func(args) 63 | -------------------------------------------------------------------------------- /zoterocleanup/_zotero.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | from configparser import SafeConfigParser 3 | from datetime import datetime 4 | from os.path import exists, expanduser, join 5 | 6 | import keyring 7 | from pyzotero.zotero import Zotero 8 | from pyzotero.zotero_errors import UserNotAuthorised 9 | 10 | DATE_FMT = "%Y-%m-%dT%XZ" 11 | KEYRING_DOMAIN = "Zotero Cleanup Library" 12 | CONFIG_PATH = join(expanduser('~'), '.zotero_cleanup.cfg') 13 | 14 | 15 | def connect(library=None, api_key=None): 16 | config_library = get_library() 17 | if library is None: 18 | library = config_library 19 | else: 20 | library = str(library) 21 | # user input 22 | if library is None: 23 | while True: 24 | library = raw_input("Library ID: ") 25 | if library and library.isdigit(): 26 | break 27 | print("Library needs to be a sequence of digits, not %r" % library) 28 | 29 | if api_key is None: 30 | api_key = keyring.get_password(KEYRING_DOMAIN, library) 31 | api_key_changed = False 32 | else: 33 | api_key_changed = True 34 | 35 | msg_printed = False 36 | while True: 37 | if not api_key: 38 | if not msg_printed: 39 | print("Please enter the library API key " 40 | "(see https://www.zotero.org/settings/keys/new)") 41 | msg_printed = True 42 | api_key = raw_input("Library API key (ctrl-c to abort): ") 43 | 44 | z = Zotero(library, 'user', api_key, True) 45 | try: 46 | z.num_items() 47 | except UserNotAuthorised: 48 | print("Connection refused, invalid API key...") 49 | api_key = None 50 | else: 51 | # store new configuration 52 | if library != config_library: 53 | set_library(library) 54 | if api_key_changed: 55 | keyring.set_password(KEYRING_DOMAIN, library, api_key) 56 | return z 57 | 58 | 59 | def date_added(item): 60 | return datetime.strptime(item['data']['dateAdded'], DATE_FMT) 61 | 62 | 63 | def get_library(): 64 | config = SafeConfigParser() 65 | if exists(CONFIG_PATH): 66 | config.read(CONFIG_PATH) 67 | return config.get('library', 'id') 68 | 69 | 70 | def set_library(library): 71 | config = SafeConfigParser() 72 | config.add_section('library') 73 | config.set('library', 'id', library) 74 | config.write(open(CONFIG_PATH, 'w')) 75 | -------------------------------------------------------------------------------- /zoterocleanup/_tags.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | from collections import defaultdict 3 | 4 | from ._utils import ask 5 | from ._zotero import connect 6 | 7 | 8 | def merge_duplicate_tags(library=None): 9 | "Merge tags that differ only in capitalization" 10 | z = connect(library) 11 | 12 | print("Retrieving Library...") 13 | items = z.everything(z.top()) 14 | 15 | tags = collect_tags(items) 16 | mod = find_duplicates(tags) 17 | updated = update_tags(mod, items) 18 | 19 | if updated: 20 | print("Updating library...") 21 | for i in updated: 22 | z.update_item(items[i]) 23 | else: 24 | print("No items found.") 25 | 26 | 27 | def collect_tags(items): 28 | "Return a set of all tags" 29 | out = set() 30 | for item in items: 31 | out.update((i['tag'] for i in item['data']['tags'])) 32 | return out 33 | 34 | 35 | def find_duplicates(tags): 36 | equal = defaultdict(set) 37 | for tag in tags: 38 | equal[tag.lower()].add(tag) 39 | 40 | conflicts = {key: list(versions) for key, versions in equal.iteritems() if 41 | len(versions) > 1} 42 | 43 | modifications = {} 44 | # ask user what to do 45 | for key, versions in conflicts.iteritems(): 46 | # acronyms 47 | upper = versions[0].upper() 48 | capitalized = versions[0].capitalize() 49 | for ideal in (upper, capitalized): 50 | if any(v == ideal for v in versions): 51 | modifications.update({v: ideal for v in versions if v != ideal}) 52 | break 53 | else: 54 | versions.append(capitalized) 55 | print('\n'.join("%i: %s" % item for item in enumerate(versions, 1))) 56 | options = [str(i) for i in xrange(1, len(versions) + 1)] + ['s'] 57 | cmd = ask("# to keep (or [s]kip)", options) 58 | if cmd != 's': 59 | target = versions[int(cmd) - 1] 60 | for v in versions: 61 | if v != target: 62 | modifications[v] = target 63 | return modifications 64 | 65 | 66 | def remove_stars(library=None): 67 | z = connect(library) 68 | print("Retrieving Library...") 69 | items = z.everything(z.top()) 70 | changed_items = set() 71 | 72 | for item_i, item in enumerate(items): 73 | tags = item['data']['tags'] 74 | labels = [t['tag'] for t in tags] 75 | for i in xrange(len(tags)-1, -1, -1): 76 | label = tags[i]['tag'] 77 | if label.startswith('*'): 78 | new = label[1:] 79 | if new in labels: 80 | print("rm " + label) 81 | del tags[i] 82 | else: 83 | print(label + ' -> ' + new) 84 | tags[i]['tag'] = new 85 | changed_items.add(item_i) 86 | 87 | # ask for confirmation/apply 88 | if ask("Apply changes?") == 'y': 89 | print("Updating library...") 90 | for i in changed_items: 91 | z.update_item(items[i]) 92 | 93 | 94 | def update_tags(modifications, items): 95 | "Apply {old: new} modifications" 96 | updated_items = set() 97 | 98 | for item_i, item in enumerate(items): 99 | tags = [i['tag'] for i in item['data']['tags']] 100 | for i, tag in reversed(list(enumerate(tags))): 101 | if tag in modifications: 102 | new = modifications[tag] 103 | if new in tags: 104 | del item['data']['tags'][i] 105 | else: 106 | item['data']['tags'][i] = {u'tag': new} 107 | updated_items.add(item_i) 108 | return updated_items 109 | -------------------------------------------------------------------------------- /zoterocleanup/_authors.py: -------------------------------------------------------------------------------- 1 | # Author: Christian Brodbeck 2 | from collections import defaultdict 3 | from itertools import izip_longest 4 | import re 5 | 6 | from ._utils import ask 7 | from ._zotero import connect 8 | 9 | PREFIXES = ('van', 'von', 'de') 10 | 11 | 12 | class Given(object): 13 | 14 | def __init__(self, names): 15 | names_ = [] 16 | for name in names: # can't just capitalize ("DuBois") 17 | if name[0].islower(): 18 | name = name[0].capitalize() + name[1:] 19 | names_.append(name) 20 | self.names = tuple(names_) 21 | self.namestring = ' '.join(self.names) 22 | 23 | @classmethod 24 | def from_namestring(cls, namestring): 25 | return cls(re.findall(u"([\w-]+).?\s*", namestring, re.UNICODE)) 26 | 27 | def __eq__(self, other): 28 | return self.names == other.names 29 | 30 | def __hash__(self): 31 | return hash(self.namestring) 32 | 33 | def is_more_complete_than(self, other): 34 | if self == other: 35 | return False 36 | elif len(self.names) < len(other.names): 37 | return False 38 | 39 | for sname, oname in izip_longest(self.names, other.names, fillvalue=''): 40 | if not sname.startswith(oname): 41 | return False 42 | return True 43 | 44 | def should_merge(self, other): 45 | if self == other: 46 | return False 47 | 48 | for sname, oname in izip_longest(self.names, other.names, fillvalue=''): 49 | if not sname.startswith(oname) or oname.startswith(sname): 50 | return False 51 | 52 | return True 53 | 54 | def union(self, other): 55 | out = [] 56 | for sname, oname in izip_longest(self.names, other.names, fillvalue=''): 57 | if len(sname) > len(oname): 58 | if sname.startswith(oname): 59 | out.append(sname) 60 | else: 61 | raise ValueError("can't merge %s and %s" % (sname, oname)) 62 | elif oname.startswith(sname): 63 | out.append(oname) 64 | else: 65 | raise ValueError("can't merge %s and %s" % (sname, oname)) 66 | return Given(out) 67 | 68 | 69 | def merge_authors(library=None, verbose=True): 70 | """Find and merge different variations of author names 71 | 72 | Parameters 73 | ---------- 74 | library : str | int 75 | Library ID. 76 | 77 | Notes 78 | ----- 79 | Asks for confirmation Before applying any changes. The following operations 80 | are performed: 81 | 82 | - If a name is stored as single string, decompose it into first and last 83 | name 84 | - Fix names where part of the last name was assigned to the first name 85 | (e.g., "van" in "van Gogh") 86 | - Find missing middle initials. 87 | """ 88 | z = connect(library) 89 | print("Retrieving Library...") 90 | items = z.everything(z.top()) 91 | 92 | # filter out ones wthout authors 93 | items = [i for i in items if 'creators' in i['data']] 94 | 95 | print("Resolving author names...") 96 | changed_items = set() 97 | 98 | # decompose full names 99 | decomposed = {} # full -> first, last 100 | ignored = set() 101 | for i, item in enumerate(items): 102 | for person in item['data']['creators']: 103 | if 'name' in person: 104 | if 'firstName' in person or 'lastName' in person: 105 | raise RuntimeError("name and firstName or lastName") 106 | name = person.pop('name') 107 | if name in decomposed: 108 | last, first = decomposed[name] 109 | else: 110 | if name.count(',') == 1: 111 | last, first = map(unicode.strip, name.split(',')) 112 | elif name.count(' ') == 1 and name.count(',') == 0: 113 | first, last = map(unicode.strip, name.split(' ')) 114 | else: 115 | m = re.match("(\w+ \w.?) (\w+)", name, re.UNICODE) 116 | if m: 117 | first, last = m.groups() 118 | else: 119 | ignored.add(name) 120 | continue 121 | decomposed[name] = (last, first) 122 | person['firstName'] = first 123 | person['lastName'] = last 124 | changed_items.add(i) 125 | 126 | # fix decomposition 127 | re_decomposed = {} # {(first, last): (first, last)} 128 | for i, item in enumerate(items): 129 | for person in item['data']['creators']: 130 | if 'name' in person: 131 | continue 132 | first = person['firstName'] 133 | last = person['lastName'] 134 | for prefix in PREFIXES: 135 | if first.lower().endswith(' %s' % prefix): 136 | n = len(prefix) 137 | new_first = first[:-(n + 1)] 138 | new_last = first[-n:] + ' ' + last 139 | person['firstName'] = new_first 140 | person['lastName'] = new_last 141 | changed_items.add(i) 142 | re_decomposed[(last, first)] = (new_last, new_first) 143 | 144 | # normalize and collect all names 145 | people = {} # last -> set(firsts as Given) 146 | normalized = defaultdict(dict) # {last: {first -> normalized_first}} 147 | for i, item in enumerate(items): 148 | for person in item['data']['creators']: 149 | if 'name' in person: 150 | continue 151 | first = person['firstName'] 152 | last = person['lastName'] 153 | first_g = Given.from_namestring(first) 154 | # normalize 155 | if first != first_g.namestring: 156 | person['firstName'] = first_g.namestring 157 | normalized[last][first] = first_g.namestring 158 | changed_items.add(i) 159 | # add to people 160 | if last in people: 161 | people[last].add(first_g) 162 | else: 163 | people[last] = {first_g} 164 | 165 | # find first names to merge 166 | merge = defaultdict(dict) # {last: {first_src: first_dst}} 167 | for last, first_names in people.iteritems(): 168 | if len(first_names) > 1: 169 | first_names = tuple(first_names) 170 | for i, first1 in enumerate(first_names, 1): 171 | for first2 in first_names[i:]: 172 | if first1.is_more_complete_than(first2): 173 | merge[last][first2.namestring] = first1.namestring 174 | elif first2.is_more_complete_than(first1): 175 | merge[last][first1.namestring] = first2.namestring 176 | elif first1.should_merge(first2): 177 | u = first1.union(first2) 178 | merge[last][first1.namestring] = u.namestring 179 | merge[last][first2.namestring] = u.namestring 180 | 181 | # apply first name merges 182 | for i, item in enumerate(items): 183 | for person in item['data']['creators']: 184 | if 'name' in person: 185 | continue 186 | last = person['lastName'] 187 | if last not in merge: 188 | continue 189 | first = person['firstName'] 190 | if first not in merge[last]: 191 | continue 192 | person['firstName'] = merge[last][first] 193 | changed_items.add(i) 194 | 195 | if not changed_items: 196 | print("No changes found.") 197 | return 198 | 199 | # print changes 200 | if decomposed: 201 | print "Decomposed:" 202 | for full in sorted(decomposed): 203 | last, first = decomposed[full] 204 | print " %s -> %s, %s" % (full, last, first) 205 | if re_decomposed: 206 | print "Re-Decomposed:" 207 | for (l, f) in sorted(re_decomposed): 208 | nl, nf = re_decomposed[(l, f)] 209 | print " %s, %s -> %s, %s" % (l, f, nl, nf) 210 | if verbose: 211 | # find people remaining separate 212 | remaining_separate = [] 213 | for last in sorted(people): 214 | first_names = people[last] 215 | remaining_first = [] 216 | if len(first_names) > 1: 217 | mapping = merge.get(last, {}) 218 | for first in first_names: 219 | if first.namestring not in mapping: 220 | remaining_first.append(first.namestring) 221 | if len(remaining_first) > 1: 222 | remaining_separate.append((last, remaining_first)) 223 | if remaining_separate: 224 | print "Remaining separate:" 225 | for last, remaining_first in remaining_separate: 226 | print " %s: %s" % (last, ', '.join(remaining_first)) 227 | if normalized: 228 | print "Normalized:" 229 | for last in sorted(normalized): 230 | mapping = normalized[last] 231 | print " %s" % last 232 | for pair in mapping.iteritems(): 233 | print " %s -> %s" % pair 234 | if merge: 235 | print "To be merged:" 236 | for last in sorted(merge): 237 | print " %s" % last 238 | for pair in merge[last].iteritems(): 239 | print " %s -> %s" % pair 240 | if ignored: 241 | print("Ignored author names:") 242 | for name in sorted(ignored): 243 | print(" %s" % name) 244 | 245 | # ask for confirmation/apply 246 | if ask("Apply changes?") == 'y': 247 | print("Updating library...") 248 | for i in changed_items: 249 | z.update_item(items[i]) 250 | 251 | 252 | def rename_author(last, first, newlast=None, newfirst=None, library=None): 253 | """Rename a single author 254 | 255 | Parameters 256 | ---------- 257 | last : str 258 | Author's last name. 259 | first : str 260 | Author's given name(s). 261 | newlast : str (optional) 262 | New entry for author's last name. 263 | newfirst : str (optional) 264 | New entry for author's given name(s). 265 | library : str | int 266 | Library ID. 267 | """ 268 | if newlast is None and newfirst is None: 269 | raise ValueError("Need to change at least one component") 270 | elif newlast is None: 271 | print "%s, %s -> %s, %s" % (last, first, last, newfirst) 272 | elif newfirst is None: 273 | print "%s, %s -> %s, %s" % (last, first, newlast, first) 274 | else: 275 | print "%s, %s -> %s, %s" % (last, first, newlast, newfirst) 276 | 277 | z = connect(library) 278 | print("Retrieving Library...") 279 | items = z.everything(z.top()) 280 | 281 | changed_items = [] 282 | for i, item in enumerate(items): 283 | for person in item['data']['creators']: 284 | if person['lastName'] == last and person['firstName'] == first: 285 | if newlast is not None: 286 | person['lastName'] = newlast 287 | if newfirst is not None: 288 | person['firstName'] = newfirst 289 | changed_items.append(item) 290 | 291 | if ask("Change %i occurrences?" % len(changed_items)) == 'y': 292 | print("Updating library...") 293 | for item in changed_items: 294 | z.update_item(item) 295 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Setuptools bootstrapping installer. 5 | 6 | Run this script to install or upgrade setuptools. 7 | """ 8 | 9 | import os 10 | import shutil 11 | import sys 12 | import tempfile 13 | import zipfile 14 | import optparse 15 | import subprocess 16 | import platform 17 | import textwrap 18 | import contextlib 19 | import warnings 20 | 21 | from distutils import log 22 | 23 | try: 24 | from urllib.request import urlopen 25 | except ImportError: 26 | from urllib2 import urlopen 27 | 28 | try: 29 | from site import USER_SITE 30 | except ImportError: 31 | USER_SITE = None 32 | 33 | DEFAULT_VERSION = "12.3" 34 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" 35 | 36 | 37 | def _python_cmd(*args): 38 | """ 39 | Execute a command. 40 | 41 | Return True if the command succeeded. 42 | """ 43 | args = (sys.executable,) + args 44 | return subprocess.call(args) == 0 45 | 46 | 47 | def _install(archive_filename, install_args=()): 48 | """Install Setuptools.""" 49 | with archive_context(archive_filename): 50 | # installing 51 | log.warn('Installing Setuptools') 52 | if not _python_cmd('setup.py', 'install', *install_args): 53 | log.warn('Something went wrong during the installation.') 54 | log.warn('See the error message above.') 55 | # exitcode will be 2 56 | return 2 57 | 58 | 59 | def _build_egg(egg, archive_filename, to_dir): 60 | """Build Setuptools egg.""" 61 | with archive_context(archive_filename): 62 | # building an egg 63 | log.warn('Building a Setuptools egg in %s', to_dir) 64 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 65 | # returning the result 66 | log.warn(egg) 67 | if not os.path.exists(egg): 68 | raise IOError('Could not build the egg.') 69 | 70 | 71 | class ContextualZipFile(zipfile.ZipFile): 72 | 73 | """Supplement ZipFile class to support context manager for Python 2.6.""" 74 | 75 | def __enter__(self): 76 | return self 77 | 78 | def __exit__(self, type, value, traceback): 79 | self.close() 80 | 81 | def __new__(cls, *args, **kwargs): 82 | """Construct a ZipFile or ContextualZipFile as appropriate.""" 83 | if hasattr(zipfile.ZipFile, '__exit__'): 84 | return zipfile.ZipFile(*args, **kwargs) 85 | return super(ContextualZipFile, cls).__new__(cls) 86 | 87 | 88 | @contextlib.contextmanager 89 | def archive_context(filename): 90 | """ 91 | Unzip filename to a temporary directory, set to the cwd. 92 | 93 | The unzipped target is cleaned up after. 94 | """ 95 | tmpdir = tempfile.mkdtemp() 96 | log.warn('Extracting in %s', tmpdir) 97 | old_wd = os.getcwd() 98 | try: 99 | os.chdir(tmpdir) 100 | with ContextualZipFile(filename) as archive: 101 | archive.extractall() 102 | 103 | # going in the directory 104 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 105 | os.chdir(subdir) 106 | log.warn('Now working in %s', subdir) 107 | yield 108 | 109 | finally: 110 | os.chdir(old_wd) 111 | shutil.rmtree(tmpdir) 112 | 113 | 114 | def _do_download(version, download_base, to_dir, download_delay): 115 | """Download Setuptools.""" 116 | egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' 117 | % (version, sys.version_info[0], sys.version_info[1])) 118 | if not os.path.exists(egg): 119 | archive = download_setuptools(version, download_base, 120 | to_dir, download_delay) 121 | _build_egg(egg, archive, to_dir) 122 | sys.path.insert(0, egg) 123 | 124 | # Remove previously-imported pkg_resources if present (see 125 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). 126 | if 'pkg_resources' in sys.modules: 127 | del sys.modules['pkg_resources'] 128 | 129 | import setuptools 130 | setuptools.bootstrap_install_from = egg 131 | 132 | 133 | def use_setuptools( 134 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, 135 | to_dir=os.curdir, download_delay=15): 136 | """ 137 | Ensure that a setuptools version is installed. 138 | 139 | Return None. Raise SystemExit if the requested version 140 | or later cannot be installed. 141 | """ 142 | to_dir = os.path.abspath(to_dir) 143 | 144 | # prior to importing, capture the module state for 145 | # representative modules. 146 | rep_modules = 'pkg_resources', 'setuptools' 147 | imported = set(sys.modules).intersection(rep_modules) 148 | 149 | try: 150 | import pkg_resources 151 | pkg_resources.require("setuptools>=" + version) 152 | # a suitable version is already installed 153 | return 154 | except ImportError: 155 | # pkg_resources not available; setuptools is not installed; download 156 | pass 157 | except pkg_resources.DistributionNotFound: 158 | # no version of setuptools was found; allow download 159 | pass 160 | except pkg_resources.VersionConflict as VC_err: 161 | if imported: 162 | _conflict_bail(VC_err, version) 163 | 164 | # otherwise, unload pkg_resources to allow the downloaded version to 165 | # take precedence. 166 | del pkg_resources 167 | _unload_pkg_resources() 168 | 169 | return _do_download(version, download_base, to_dir, download_delay) 170 | 171 | 172 | def _conflict_bail(VC_err, version): 173 | """ 174 | Setuptools was imported prior to invocation, so it is 175 | unsafe to unload it. Bail out. 176 | """ 177 | conflict_tmpl = textwrap.dedent(""" 178 | The required version of setuptools (>={version}) is not available, 179 | and can't be installed while this script is running. Please 180 | install a more recent version first, using 181 | 'easy_install -U setuptools'. 182 | 183 | (Currently using {VC_err.args[0]!r}) 184 | """) 185 | msg = conflict_tmpl.format(**locals()) 186 | sys.stderr.write(msg) 187 | sys.exit(2) 188 | 189 | 190 | def _unload_pkg_resources(): 191 | del_modules = [ 192 | name for name in sys.modules 193 | if name.startswith('pkg_resources') 194 | ] 195 | for mod_name in del_modules: 196 | del sys.modules[mod_name] 197 | 198 | 199 | def _clean_check(cmd, target): 200 | """ 201 | Run the command to download target. 202 | 203 | If the command fails, clean up before re-raising the error. 204 | """ 205 | try: 206 | subprocess.check_call(cmd) 207 | except subprocess.CalledProcessError: 208 | if os.access(target, os.F_OK): 209 | os.unlink(target) 210 | raise 211 | 212 | 213 | def download_file_powershell(url, target): 214 | """ 215 | Download the file at url to target using Powershell. 216 | 217 | Powershell will validate trust. 218 | Raise an exception if the command cannot complete. 219 | """ 220 | target = os.path.abspath(target) 221 | ps_cmd = ( 222 | "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " 223 | "[System.Net.CredentialCache]::DefaultCredentials; " 224 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" 225 | % vars() 226 | ) 227 | cmd = [ 228 | 'powershell', 229 | '-Command', 230 | ps_cmd, 231 | ] 232 | _clean_check(cmd, target) 233 | 234 | 235 | def has_powershell(): 236 | """Determine if Powershell is available.""" 237 | if platform.system() != 'Windows': 238 | return False 239 | cmd = ['powershell', '-Command', 'echo test'] 240 | with open(os.path.devnull, 'wb') as devnull: 241 | try: 242 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 243 | except Exception: 244 | return False 245 | return True 246 | download_file_powershell.viable = has_powershell 247 | 248 | 249 | def download_file_curl(url, target): 250 | cmd = ['curl', url, '--silent', '--output', target] 251 | _clean_check(cmd, target) 252 | 253 | 254 | def has_curl(): 255 | cmd = ['curl', '--version'] 256 | with open(os.path.devnull, 'wb') as devnull: 257 | try: 258 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 259 | except Exception: 260 | return False 261 | return True 262 | download_file_curl.viable = has_curl 263 | 264 | 265 | def download_file_wget(url, target): 266 | cmd = ['wget', url, '--quiet', '--output-document', target] 267 | _clean_check(cmd, target) 268 | 269 | 270 | def has_wget(): 271 | cmd = ['wget', '--version'] 272 | with open(os.path.devnull, 'wb') as devnull: 273 | try: 274 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 275 | except Exception: 276 | return False 277 | return True 278 | download_file_wget.viable = has_wget 279 | 280 | 281 | def download_file_insecure(url, target): 282 | """Use Python to download the file, without connection authentication.""" 283 | src = urlopen(url) 284 | try: 285 | # Read all the data in one block. 286 | data = src.read() 287 | finally: 288 | src.close() 289 | 290 | # Write all the data in one block to avoid creating a partial file. 291 | with open(target, "wb") as dst: 292 | dst.write(data) 293 | download_file_insecure.viable = lambda: True 294 | 295 | 296 | def get_best_downloader(): 297 | downloaders = ( 298 | download_file_powershell, 299 | download_file_curl, 300 | download_file_wget, 301 | download_file_insecure, 302 | ) 303 | viable_downloaders = (dl for dl in downloaders if dl.viable()) 304 | return next(viable_downloaders, None) 305 | 306 | 307 | def download_setuptools( 308 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, 309 | to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): 310 | """ 311 | Download setuptools from a specified location and return its filename. 312 | 313 | `version` should be a valid setuptools version number that is available 314 | as an sdist for download under the `download_base` URL (which should end 315 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 316 | `delay` is the number of seconds to pause before an actual download 317 | attempt. 318 | 319 | ``downloader_factory`` should be a function taking no arguments and 320 | returning a function for downloading a URL to a target. 321 | """ 322 | # making sure we use the absolute path 323 | to_dir = os.path.abspath(to_dir) 324 | zip_name = "setuptools-%s.zip" % version 325 | url = download_base + zip_name 326 | saveto = os.path.join(to_dir, zip_name) 327 | if not os.path.exists(saveto): # Avoid repeated downloads 328 | log.warn("Downloading %s", url) 329 | downloader = downloader_factory() 330 | downloader(url, saveto) 331 | return os.path.realpath(saveto) 332 | 333 | 334 | def _build_install_args(options): 335 | """ 336 | Build the arguments to 'python setup.py install' on the setuptools package. 337 | 338 | Returns list of command line arguments. 339 | """ 340 | return ['--user'] if options.user_install else [] 341 | 342 | 343 | def _parse_args(): 344 | """Parse the command line for options.""" 345 | parser = optparse.OptionParser() 346 | parser.add_option( 347 | '--user', dest='user_install', action='store_true', default=False, 348 | help='install in user site package (requires Python 2.6 or later)') 349 | parser.add_option( 350 | '--download-base', dest='download_base', metavar="URL", 351 | default=DEFAULT_URL, 352 | help='alternative URL from where to download the setuptools package') 353 | parser.add_option( 354 | '--insecure', dest='downloader_factory', action='store_const', 355 | const=lambda: download_file_insecure, default=get_best_downloader, 356 | help='Use internal, non-validating downloader' 357 | ) 358 | parser.add_option( 359 | '--version', help="Specify which version to download", 360 | default=DEFAULT_VERSION, 361 | ) 362 | options, args = parser.parse_args() 363 | # positional arguments are ignored 364 | return options 365 | 366 | 367 | def main(): 368 | """Install or upgrade setuptools and EasyInstall.""" 369 | options = _parse_args() 370 | archive = download_setuptools( 371 | version=options.version, 372 | download_base=options.download_base, 373 | downloader_factory=options.downloader_factory, 374 | ) 375 | return _install(archive, _build_install_args(options)) 376 | 377 | if __name__ == '__main__': 378 | sys.exit(main()) 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | --------------------------------------------------------------------------------