diff --git a/doll/__main__.py b/doll/__main__.py new file mode 100644 index 0000000..f6368a2 --- /dev/null +++ b/doll/__main__.py @@ -0,0 +1,59 @@ +import doll.data +import doll.input_parser +import doll.parse_test +import argparse + +description = """ +DDDDDDDDDDDDD LLLLLLLLLL LLLLLLLLLL +D::::::::::::DDD L::::::::L L::::::::L +D:::::::::::::::DD L::::::::L L::::::::L +DDD:::::DDDDD:::::D LL::::::LL LL::::::LL + D:::::D D:::::D ooooooooooo L::::L L::::L + D:::::D D:::::D oo:::::::::::oo L::::L L::::L + D:::::D D:::::Do:::::::::::::::o L::::L L::::L + D:::::D D:::::Do:::::ooooo:::::o L::::L L::::L + D:::::D D:::::Do::::o o::::o L::::L L::::L + D:::::D D:::::Do::::o o::::o L::::L L::::L + D:::::D D:::::Do::::o o::::o L::::L L::::L + D:::::D D:::::D o::::o o::::o L::::L LLLL L::::L LLLL +DDD:::::DDDDD:::::D o:::::ooooo:::::oLL::::::LLLLLL:::LLL::::::LLLLLL:::L +D:::::::::::::::DD o:::::::::::::::oL::::::::::::::::LL::::::::::::::::L +D::::::::::::DDD oo:::::::::::oo L::::::::::::::::LL::::::::::::::::L +DDDDDDDDDDDDD ooooooooooo LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL + +The Database of Latin Lexicon + +An implementation of William Whitaker\'s Words\' data model in Python. + +This program comprises three parts: + - Downloader, to download the Words source files, contained in the 'data' + module + - Model, the sqlalchemy model of the data, contained in the 'db' module + - Parser, which ingests the Words source files and populates the database + with them, via the sqlalchemy model. This is contained in the + input_parser module. + +In addition, there is a parse_test script in the root directory, +demonstrating a use of the package to replicate certain functionality +of Words. +""" + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument("-f", "--force", action='store_true', help="Force a re-download of the words.zip file") + parser.add_argument("-b", "--build", action='store_true', help="Build the database") + parser.add_argument("-p", "--parse", action='store_true', help="Run the example parser") + + args = parser.parse_args() + + if args.force: + doll.data.download(create_dir=True) + if args.build: + doll.input_parser.parse_all_inputs(commit_changes=True) + if args.parse: + while True: + word = input('Enter a word to parse or type quit() to exit:\n=> ') + if word == 'quit()': + break + doll.parse_test.parse_word(word) diff --git a/doll/config.py b/doll/config.py new file mode 100644 index 0000000..45574eb --- /dev/null +++ b/doll/config.py @@ -0,0 +1,13 @@ +""" + + DOLL Config File + + This file contains the user-configurable elements of the Dictionary Of Latin Lexicon + +""" + +config = { + 'db_file': 'doll.db', + 'sqlalchemy.pool_recycle': '50', + 'sqlalchemy.echo': 'false' +} diff --git a/doll/data/__init__.py b/doll/data/__init__.py new file mode 100644 index 0000000..d0892c6 --- /dev/null +++ b/doll/data/__init__.py @@ -0,0 +1,78 @@ +from os import mkdir +from sys import stdout +from os.path import exists, expanduser, isdir, join +from urllib.request import urlopen +from zipfile import ZipFile + + +def _doll_dir(create: bool = False): + """Find or create the doll data directory + + :param create: whether to create the directory if it doesn't exist + :type create: bool + + :return the directory, whether pre-existing or just-created + """ + + doll_dir = expanduser("~/.doll") + + if not exists(doll_dir): + if not create: + raise RuntimeError("doll data directory does not exist and was not created at {}\n" + "(rerun with create=True to create.)".format(doll_dir)) + print("Creating ~/.doll directory") + try: + mkdir(doll_dir) + except OSError: + raise RuntimeError("Could not create doll data directory at {}".format(doll_dir)) + else: + if not isdir(doll_dir): + raise RuntimeError("{0} exists but is not a directory".format(doll_dir)) + else: + print("~/.doll directory already exists and was not created.") + + return doll_dir + + +def download(create_dir: bool = False): + """Download and extract the Words source files + + :param create_dir: whether to create the directory if it doesn't exist + :type create_dir: bool + + :return None + """ + + words_all = 'wordsall' + words_url = 'http://archives.nd.edu/whitaker/wordsall.zip' + data_dir = _doll_dir(create=create_dir) + + url = urlopen(words_url) + + # Download the file + with open(join(data_dir, words_all + '.zip'), 'wb') as file: + file_size = int(url.headers["Content-Length"]) + print('Downloading {}.zip ({:,} bytes)'.format(words_all, file_size)) + + fetch_size = 0 + block_size = 1024 * 64 + + while True: + data = url.read(block_size) + if not data: + break + + fetch_size += len(data) + file.write(data) + + status = '\r{:12,} bytes [{:5.1f}%]'.format(fetch_size, fetch_size * 100.0 / file_size) + stdout.write(status) + stdout.flush() + + # Unpack the file + print('\nUnpacking {}'.format(words_all + '.zip')) + + with ZipFile(join(data_dir, words_all + '.zip'), 'r') as zip_file: + zip_file.extractall(join(data_dir, words_all)) + + print('{} downloaded and extracted at {}'.format(words_all, data_dir)) diff --git a/doll/db/__init__.py b/doll/db/__init__.py index 1e14e62..aa854cf 100644 --- a/doll/db/__init__.py +++ b/doll/db/__init__.py @@ -1,25 +1,20 @@ -__author__ = 'Matthew Badger' - - -from os.path import dirname +from os.path import expanduser from sqlalchemy import engine_from_config from sqlalchemy.orm import sessionmaker +from ..config import config +from .model import * -from doll.db.config import config -from doll.db.model import * - - -'''Connection class - - Connects to the database in the root folder of the application - -''' # Connects to the database class Connection: + """Connection + + Connects to the database in the user's .doll directory + """ + config = config - config['sqlalchemy.url'] = 'sqlite:///' + dirname(__file__) + '/' + config['db_file'] + config['sqlalchemy.url'] = 'sqlite:///' + expanduser("~/.doll") + '/' + config['db_file'] __engine = engine_from_config(config, echo=False) diff --git a/doll/db/config.py b/doll/db/config.py deleted file mode 100644 index d3605bf..0000000 --- a/doll/db/config.py +++ /dev/null @@ -1,5 +0,0 @@ -config = { - 'db_file': 'doll.db', - 'sqlalchemy.pool_recycle': '50', - 'sqlalchemy.echo': 'false' -} \ No newline at end of file diff --git a/doll/db/model.py b/doll/db/model.py index ac02e3c..62e33f2 100644 --- a/doll/db/model.py +++ b/doll/db/model.py @@ -21,12 +21,12 @@ """ -__author__ = 'Matthew Badger' - from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Unicode from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.declarative import declarative_base, declared_attr +__author__ = 'Matthew Badger' + Base = declarative_base() @@ -38,7 +38,7 @@ TypeBase, which defines an id, code, name and description. Most of the type classes define no other columns. The code matches that -in Whitaker's source, and has a unique key. id is just for backrefernces +in Whitaker's source, and has a unique key. id is just for back-references by sqlalchemy. name is hopefully a better thing to present to the user than the code. @@ -51,6 +51,9 @@ class TypeBase(object): def __tablename__(self): return 'type_' + self.__name__.lower() + def __repr__(self): + return "{0}\t{1}\t{2} - {3}".format(self.id, self.code, self.name, self.description) + id = Column(Integer, primary_key=True) code = Column(String(10), unique=True) name = Column(String(50)) @@ -121,6 +124,16 @@ def __lt__(self, other): return self.order < other.order +class RealConjugation(TypeBase, Base): + """Real conjugation, which is a bit of a misnomer because + it's only a bit more real than Conjugation""" + + order = Column(Integer) + + def __lt__(self, other): + return self.order < other.order + + class Person(TypeBase, Base): """Person - First, Second or Third""" @@ -170,7 +183,6 @@ class Language(TypeBase, Base): """Languages for translation""" - """Inflection Record Classes. These classes define the inflection records, built from @@ -202,7 +214,8 @@ class Record(Base): # Other columns stem_key = Column(Integer) - ending = Column(Unicode(20, collation='BINARY')) # We use binary collation so a is not ā + ending = Column(Unicode(20, collation='BINARY')) # We use binary collation so macrons are different + simple_ending = Column(Unicode(20)) notes = Column(Unicode(200, collation='BINARY')) # Relationships @@ -524,7 +537,8 @@ class Stem(Base): name='FK_dictionary_stem_entry_id')) stem_number = Column(Integer) - stem_word = Column(Unicode(20, collation='BINARY')) # We use binary collation so a is not ā + stem_word = Column(Unicode(20, collation='BINARY')) # We use binary collation so macrons are different + stem_simple_word = Column(Unicode(20)) # Relationships entry = relationship('Entry', backref=backref('dictionary_stem')) @@ -552,7 +566,6 @@ class TranslationSet(Base): translations = relationship('Translation', backref=backref('dictionary_translation')) - # Translation class Translation(Base): """A translation of a word in a given language""" @@ -568,7 +581,6 @@ class Translation(Base): translation_set = relationship('TranslationSet', backref=backref('dictionary_translation')) - # Noun Entry class NounEntry(Base): """Noun entry in the dictionary""" @@ -713,6 +725,9 @@ class VerbEntry(Base): conjugation_code = Column(String(10), ForeignKey('type_conjugation.code', name='FK_dictionary_verb_conjugation_code')) + + realconjugation_code = Column(String(10), ForeignKey('type_realconjugation.code', + name='FK_dictionary_verb_realconjugation_code')) variant = Column(Integer) verb_kind_code = Column(String(10), ForeignKey('type_verbkind.code', @@ -766,4 +781,4 @@ class InterjectionEntry(Base): name='FK_dictionary_interjection_entry_id')) # Relationships - entry = relationship('Entry', backref=backref('dictionary_interjection')) \ No newline at end of file + entry = relationship('Entry', backref=backref('dictionary_interjection')) diff --git a/doll/input_parser/__init__.py b/doll/input_parser/__init__.py index 8a02a8a..1d5ee1f 100644 --- a/doll/input_parser/__init__.py +++ b/doll/input_parser/__init__.py @@ -1,39 +1,47 @@ -__author__ = 'Matthew' - import os +from ..input_parser.add_database_types import create_type_contents +from ..input_parser.parse_dictionary import parse_dict_file +from ..input_parser.parse_inflections import parse_inflect_file +from ..config import config + -from doll.input_parser.add_database_types import create_type_contents -from doll.input_parser.parse_dictionary import parse_dict_file -from doll.input_parser.parse_inflections import parse_inflect_file +def parse_all_inputs(words_dir: str = os.path.expanduser('~/.doll/wordsall'), commit_changes: bool = False): + """Creates the database and parses all the inputs + :param words_dir: Directory of wordsall + :type words_dir: str + :param commit_changes: Whether to commit changes to the database + :type commit_changes: bool -def parse_all_inputs(words_folder, commit_changes): - """Creates the database and parses all the inputs""" + :return None + """ # Add a trailing slash if necessary - if (words_folder[-1:] != '/'): - words_folder += '/' + if words_dir[-1:] != '/': + words_dir += '/' # First check that our words folder exists - if not os.path.isdir(words_folder): - print('Cannot find words_folder at {0}! Exiting...'.format(words_folder)) + if not os.path.isdir(words_dir): + print('Cannot find words_dir at {0}! Exiting...'.format(words_dir)) return # And then that our input files exist files_to_find = ['INFLECTS.LAT', 'DICTLINE.GEN'] - error_string = ', '.join([f for f in files_to_find if not os.path.isfile(words_folder + f)]) + error_string = ', '.join([f for f in files_to_find if not os.path.isfile(words_dir + f)]) if not error_string == '': print('Unable to find the following file(s): ' + error_string + '. Exiting...') return - '''if os.path.isfile(config['db_file']): - if not input('Database file exists, overwrite? ([Y]es/ No)')[:1] == 'Y': + if os.path.isfile(os.path.expanduser("~/.doll/") + config['db_file']): + if not input('Database file exists, overwrite? (Yes/ No)')[:1] == 'Y': print('Database file exists, exiting...') - return''' + return + else: + os.remove(os.path.expanduser("~/.doll/") + config['db_file']) create_type_contents() - parse_inflect_file(inflect_file=words_folder + 'INFLECTS.LAT', commit_changes=commit_changes) + parse_inflect_file(inflect_file=words_dir + 'INFLECTS.LAT', commit_changes=commit_changes) - parse_dict_file(dict_file=words_folder + 'DICTLINE.GEN', commit_changes=commit_changes) + parse_dict_file(dict_file=words_dir + 'DICTLINE.GEN', commit_changes=commit_changes) diff --git a/doll/input_parser/add_database_types.py b/doll/input_parser/add_database_types.py index bf5c271..d8802b2 100644 --- a/doll/input_parser/add_database_types.py +++ b/doll/input_parser/add_database_types.py @@ -184,6 +184,17 @@ def create_type_contents(): Connection.session.add(Conjugation(code=8, name='Sixth', description='', order=8)) Connection.session.add(Conjugation(code=9, name='Sixth', description='', order=9)) + Connection.session.add(RealConjugation(code=0, name='Unknown', description='', order=0)) + Connection.session.add(RealConjugation(code=1, name='First', description='', order=1)) + Connection.session.add(RealConjugation(code=2, name='Second', description='', order=2)) + Connection.session.add(RealConjugation(code=3, name='Third', description='', order=3)) + Connection.session.add(RealConjugation(code=4, name='Fourth', description='', order=5)) + Connection.session.add(RealConjugation(code=5, name='Third -io', description='', order=4)) + Connection.session.add(RealConjugation(code=6, name='Esse', description='', order=6)) + Connection.session.add(RealConjugation(code=7, name='Eo', description='', order=7)) + Connection.session.add(RealConjugation(code=8, name='Irregular', description='', order=8)) + Connection.session.add(RealConjugation(code=9, name='Other', description='', order=9)) + Connection.session.add(Person(code=0, name='Unknown', description='All, none, or unknown')) Connection.session.add(Person(code=1, name='First', description='')) Connection.session.add(Person(code=2, name='Second', description='')) diff --git a/doll/input_parser/parse_dictionary.py b/doll/input_parser/parse_dictionary.py index 02e7919..e1a7234 100644 --- a/doll/input_parser/parse_dictionary.py +++ b/doll/input_parser/parse_dictionary.py @@ -1,35 +1,67 @@ -__author__ = 'Matthew Badger' - from doll.db import Connection from doll.db.model import * import re - - -def parse_translation(session, language, entry, translation): - """Parses the translation line and creates the Translation and TranslationSet objects""" - - for ts in [ts for ts in map(str.strip, translation.split(';')) if len(ts) > 0]: - - # Check if the translation set includes an area - area_regex = re.match('\s([A-Z]):', ts) - if area_regex is not None: - area = session.query(WordArea).filter(WordArea.code == area_regex.group(0)).first() - translation_set = TranslationSet(entry=entry, - area=area, - language=language) +from tqdm import tqdm + + +class Parser: + _regex = re.compile('\s([A-Z]):') + + def __init__(self, session): + self.session = session + self._word_areas = {word_area.code: word_area for word_area in session.query(WordArea).all()} + + def parse_translation(self, language, entry, translation): + """Parses the translation line and creates the Translation and TranslationSet objects + + :param language + :param entry + :param translation + """ + + for ts in [ts for ts in map(str.strip, translation.split(';')) if len(ts) > 0]: + + # Check if the translation set includes an area + area_regex = __class__._regex.match(ts) + if area_regex is not None: + area = self._word_areas[area_regex.group(0)] + translation_set = TranslationSet(entry=entry, + area=area, + language=language) + else: + translation_set = TranslationSet(entry=entry, + language=language) + + self.session.add(translation_set) + + for t in [t for t in map(str.strip, ts.split(',')) if len(t) > 0]: + self.session.add(Translation(translation_set=translation_set, translation=t)) + + @staticmethod + def verb_real_conjugation(present_stem: str, conjugation_code: str, variant: int) -> int: + """Calculates the 'real' conjugation of a verb from its present stem, conjugation + code, and variant number + + :param present_stem + :param conjugation_code + :param variant + """ + + cc = int(conjugation_code) + + if cc in [1, 2]: # First and second are the same + return conjugation_code + elif (cc, variant) == (3, 4): # Third conjugation, fourth variant is actually fourth + return 4 + elif cc == 3 and present_stem[-1:] == 'i': # Third -io + return 5 + elif cc == 3: # Other third conjugation verbs + return 3 else: - translation_set = TranslationSet(entry=entry, - language=language) - - session.add(translation_set) - - for t in [t for t in map(str.strip, ts.split(',')) if len(t) > 0]: - session.add(Translation(translation_set=translation_set, - translation=t)) - + return cc + 1 -def parse_dict_file(dict_file, commit_changes=False): +def parse_dict_file(dict_file: str, commit_changes: bool = False): """Parses a given dictionary file. The DICTLINE.GEN file is arranged in rows as follows: @@ -50,29 +82,32 @@ def parse_dict_file(dict_file, commit_changes=False): session = Connection.session - # Open the dictionary file and loop over its lines - with open(dict_file) as f: - for line in f: - stem_list = [line[:18].strip(), - line[19:37].strip(), - line[38:56].strip(), - line[57:75].strip()] - - part_of_speech_code = line[76:82].strip() - part_of_speech_data = line[83:99].strip().split() - - age_code = line[100:101].strip() - area_code = line[102:103].strip() - location_code = line[104:105].strip() - frequency_code = line[106:107].strip() - source_code = line[108:109].strip() + parser = Parser(session=session) - translation = line[110:].strip() + language = session.query(Language).filter(Language.code == 'E').first() + + print('Parsing dictionary file') + + # Open the dictionary file and loop over its lines + with open(dict_file, encoding='windows_1252') as f: + # Start by counting the lines in the file + line_count = sum(1 for line in f) + f.seek(0) - language = session.query(Language).filter(Language.code == 'E').first() + for line in tqdm(f, total=line_count): # Create the list of stems, ignoring those that are empty or zzz - stems = [Stem(stem_number=i, stem_word=s) for i, s in enumerate(stem_list, 1) if len(s) > 0 and s != 'zzz'] + stems = [Stem(stem_number=i, stem_word=s, stem_simple_word=s) + for i, s in enumerate([line[i:i + 18].strip() for i in range(0, 58, 19)], 1) + if len(s) > 0 and s != 'zzz'] + + part_of_speech_code, part_of_speech_data = line[76:82].strip(), [p.strip() + for p in line[83:99].split()] + + # Split 100:101, ..., 108:109 + age_code, area_code, location_code, frequency_code, source_code = [line[i] + for i in range(100, 109, 2)] + translation = line[110:].strip() # Create the basic entry, i.e. everything except the part of speech data entry = Entry(part_of_speech_code=part_of_speech_code, @@ -84,10 +119,9 @@ def parse_dict_file(dict_file, commit_changes=False): translation=translation, stems=stems) - parse_translation(session=session, - language=language, - entry=entry, - translation=translation) + parser.parse_translation(language=language, + entry=entry, + translation=translation) # Create the specific entry given the part of speech if entry.part_of_speech_code == 'N': @@ -131,6 +165,9 @@ def parse_dict_file(dict_file, commit_changes=False): variant=int(part_of_speech_data[1]), verb_kind_code=part_of_speech_data[2], entry=entry) + verb_entry.realconjugation_code = Parser.verb_real_conjugation(stems[0].stem_word, + verb_entry.conjugation_code, + verb_entry.variant) session.add(verb_entry) elif entry.part_of_speech_code == 'PREP': preposition_entry = PrepositionEntry(case_code=part_of_speech_data[0], @@ -156,4 +193,5 @@ def parse_dict_file(dict_file, commit_changes=False): session.query(ConjunctionEntry).all() session.query(InterjectionEntry).all() else: + print('Committing changes to database') session.commit() \ No newline at end of file diff --git a/doll/input_parser/parse_inflections.py b/doll/input_parser/parse_inflections.py index 93606ba..5d3000b 100644 --- a/doll/input_parser/parse_inflections.py +++ b/doll/input_parser/parse_inflections.py @@ -1,7 +1,6 @@ -__author__ = 'Matthew Badger' - from doll.db import Connection from doll.db.model import * +from tqdm import tqdm """Parses the inflections input file. @@ -30,9 +29,15 @@ def parse_inflect_file(inflect_file, commit_changes=False): 'INTERJ': 1 } + print('Parsing inflections file') + # Open the inflections file and loop over its lines - with open(inflect_file) as f: - for line in f: + with open(inflect_file, encoding='windows_1252') as f: + # Start by counting the lines in the file + line_count = sum(1 for line in f) + f.seek(0) + + for line in tqdm(f, total=line_count): line_split = line.split() if len(line_split) > 0 and line_split[0][0] != '-': i = line_start[line_split[0]] diff --git a/doll/parse_test.py b/doll/parse_test.py index 488be99..4133ce2 100644 --- a/doll/parse_test.py +++ b/doll/parse_test.py @@ -1,21 +1,48 @@ -__author__ = 'Matthew Badger' - from sqlalchemy import func, or_, and_ - from doll.db import * +from enum import Enum +import argparse +import unicodedata +import string +from sqlalchemy.sql.functions import ReturnTypeFromArgs + + +class unaccent(ReturnTypeFromArgs): + pass session = Connection.session -def parse_word(word): +class ParseOption(Enum): + strict = 1, + non_strict = 2 + + +current_mode = ParseOption.non_strict + + +def remove_accents(data): + return ''.join(x for x in unicodedata.normalize('NFKD', data) if x in string.ascii_letters).lower() + + +def parse_word(word: str, current_mode: ParseOption = current_mode): # Possible entries are those where the stem joined to its appropriate endings # create our input word. - possible_entries = [(e, r, s) for e, r, s in session.query(Entry, Record, Stem) - .filter(and_(Record.part_of_speech_code == Entry.part_of_speech_code, - Record.stem_key == Stem.stem_number)) - .filter(Stem.entry_id == Entry.id) - .filter(func.substr(word, 1, func.length(Stem.stem_word)) == Stem.stem_word) - .filter(Stem.stem_word + Record.ending == word)] + + if current_mode == ParseOption.non_strict: + possible_entries = [(e, r, s) for e, r, s in session.query(Entry, Record, Stem) + .filter(and_(Record.part_of_speech_code == Entry.part_of_speech_code, + Record.stem_key == Stem.stem_number)) + .filter(Stem.entry_id == Entry.id) + .filter(func.substr(word, 1, func.length(Stem.stem_word)) == Stem.stem_word) + .filter(Stem.stem_word + Record.ending == word)] + else: + possible_entries = [(e, r, s) for e, r, s in session.query(Entry, Record, Stem) + .filter(and_(Record.part_of_speech_code == Entry.part_of_speech_code, + Record.stem_key == Stem.stem_number)) + .filter(Stem.entry_id == Entry.id) + .filter(func.substr(word, 1, func.length(Stem.stem_word)) == Stem.stem_word) + .filter(unaccent(Stem.stem_word) + unaccent(Record.ending) == unaccent(word))] # It would be preferable to get a list of possible records based on # the possible entries, then filter further by word type, however @@ -89,9 +116,19 @@ def parse_word(word): entry.translation)) -print('Welcome to Words!') -while True: - word = input('Enter a word to parse or type quit() to exit:\n') - if word == 'quit()': - break - parse_word(word) +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='An implementation of William Whitaker''s Words in Python') + parser.add_argument("-ns", "--nonstrict", action='store_true') + args = parser.parse_args() + + print('Welcome to Words!') + + if args.nonstrict: + current_mode = ParseOption.non_strict + + while True: + word = input('Enter a word to parse or type quit() to exit:\n') + if word == 'quit()': + break + parse_word(word) diff --git a/requirements.txt b/requirements.txt index 2554f80..4245d5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -sqlalchemy==1.0.8 \ No newline at end of file +sqlalchemy>=1.0.8 +tqdm>=4.10.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 0d7bc22..070913d 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,16 @@ """DoLL project""" from setuptools import find_packages, setup -setup(name = 'DoLL', - version = '0.1', - description = "Database of Latin Lexicon.", - long_description = "A database of the Latin lexicon, generated from the input files for Whitaker's Words.", - author="Matthew Badger", - url="https://github.com/badge/doll", - license = "Apache", - packages=find_packages() - ) \ No newline at end of file +setup(name='doll', + version='0.3', + description="Database of Latin Lexicon.", + long_description="A database of the Latin lexicon, generated from the input files for Whitaker's Words.", + author="Matthew Badger", + url="https://github.com/badge/doll", + license="Apache", + entry_points={ + 'console_scripts': [ + 'doll = doll.__main__:main' + ]}, + packages=find_packages() + )