web/routes/api_word.py
def words_api(dictionary_api, token):
    lookup = dictionary_api.lookup(token)
    return to_json(lookup)
web/routes/api_search.py
def search_api(searcher, query):
    media = searcher.search(query)
    return {
        'hits': [
            {
                'id': m.id,
                'title': m.title,
                'poster_url': m.poster_url
            } for m in media]
    }
api/dictionary/wordnik.py
class Wordnik:
    """API client to lookup words in wordnik.com"""

    def __init__(self, api_key, client=Fetcher(WORDNIK_URL)):
        """Constructor to prepare API connection.

        :param api_key: valid API key for wordnik.com
        :param client: client for making a batch of HTTP GET requests
        """
        self.api_key = api_key
        self.client = client

    def lookup(self, word):
        """Lookup a word in online dictionary.

        :param word: word to look up
        :returns: lookup for word
        """
        query = urlencode(OrderedDict([
            ('api_key', self.api_key),
            ('includeRelated', False),
            ('includeTags', False),
            ('limit', 200),
            ('sourceDictionaries', 'wiktionary'),
            ('useCanonical', False),
        ]))
        request_paths = ['/word.json/{}/definitions?{}'.format(word, query)]

        responses = self.client.get(request_paths)
        responses_data = [r.json() for r in responses if r and r.status_code == 200]

        definitions = []
        for response_data in responses_data:
            for entry in response_data:
                definition = WordDefinition(entry['partOfSpeech'], entry['text'])
                definitions.append(definition)

        return WordLookup(word, definitions, ATTRIBUTION)
web/routes/api_analysis.py
def analysis_api(analyser, poster_api, id):
    subtitle, analysis = analyser.analyse(id)
    poster_url = poster_api.get_movie_posters([id]).get(id)
    return Response(generate(subtitle, analysis, poster_url),
                    content_type='application/json')
domain/analyse.py
class Analyser:
    """Generate analysis report for a movie."""

    def __init__(self, loader, parser, corpus):
        self.loader = loader
        self.parser = parser
        self.corpus = corpus
        self.tagger = Tagger()
        self.excerptor = Excerptor()
        self.lemmatizer = Lemmatizer()
        self.tokenizer = Tokenizer()
        self.stop_words = set(stopwords.words('english'))

    def analyse(self, imdb_id):
        """Generate analysis report for a movie.

        :param imdb_id: IMDb ID of movie
        :return: analysis
        """

        subtitle = self.loader.load(imdb_id)
        if not subtitle:
            raise RuntimeError('no subtitle found for movie {}'.format(imdb_id))

        analysis = Analysis()
        sentences = self.parser.parse(subtitle.text)
        tokens_in_sentences = (self.tokenizer.words(s.text) for s in sentences)
        words_in_sentences = self.tagger.tag(list(tokens_in_sentences))

        for i, words in enumerate(words_in_sentences):
            for word in words:
                token, token_POS = word

                if token.lower() == 'subtitle' or not token.isalpha():
                    continue

                excerpt = self.excerptor.excerpt(sentences, i, token)

                if token in self.stop_words:
                    analysis.ignore(word, excerpt, WordIgnoreType.STOPWORD)
                    continue

                if token_POS is PartOfSpeach.OTHER:
                    analysis.ignore(word, excerpt, WordIgnoreType.UNKNOWN_TYPE)
                    continue

                lemma = self.lemmatizer.lemmatize(token, token_POS)
                if not is_real_word(lemma):
                    analysis.ignore(word, excerpt, WordIgnoreType.UNKNOWN)
                    continue

                lemma_lang_freq = self.corpus.freq(lemma)
                if lemma_lang_freq == 0:
                    analysis.ignore(word, excerpt, WordIgnoreType.UNKNOWN_FREQ)
                    continue

                difficulty = self.corpus.to_difficulty(lemma_lang_freq)
                analysis.add(
                    Word(lemma, token_POS),
                    excerpt,
                    lemma_lang_freq,
                    difficulty)

        return subtitle, analysis
domain/corpus.py
class CorpusDatabase:
    FULL = 'corpora/en.txt'
    MIN = 'corpora/en_min.txt'
web/routes/bootstrap.py
def bootstrap():
    return render_template('bootstrap.html')
domain/load.py
class Loader:
    """Load movie subtitle."""

    def __init__(self, subtitle_api):
        """
        :param subtitle_api: API to load movie subtitles
        """
        self.api = subtitle_api

    def load(self, imdb_id):
        """Load movie subtitle.

        :param movie_id: IMDb ID of movie
        :return: movie subtitle
        """
        all_subtitles = self.api.find_subtitles_for_movie(imdb_id)
        if not all_subtitles:
            return None

        subtitle = self._pick_best_subtitle(all_subtitles)
        if not subtitle:
            return None

        subtitle.text = self.api.load_text(subtitle.id, subtitle.encoding)
        return subtitle

    def _pick_best_subtitle(self, subtitles):
        valid_subtitles = [s for s in subtitles if self._is_valid_subtitle(s)]
        if not valid_subtitles:
            return None

        valid_subtitles_by_dls = sorted(valid_subtitles, key=lambda s: s.downloads)
        valid_subtitles_with_most_dls = valid_subtitles_by_dls[-1]
        return valid_subtitles_with_most_dls

    def _is_valid_subtitle(self, s):
        return not s.partial and \
            s.format == SUPPORTED_SUBTITLE_FORMAT
domain/parse.py
class Parser:
    """Parse movie subtitle into sentences."""

    def __init__(self):
        self.tokenizer = Tokenizer()

    def parse(self, subtitle):
        """Parse movie subtitle.

        :param subtitle: movie subtitle text
        :return: list of sentences
        """
        lines, text = self._parse_lines(subtitle)
        tokens = self.tokenizer.sentences(text)
        return self._to_sentences(lines, tokens)

    def _parse_lines(self, subtitle):
        lines = []
        text_buffer = []
        for entry in srt.parse(subtitle.strip()):
            for line in entry.content.split("\n"):
                line_text = line.strip()
                if line_text.startswith('...'):
                    line_text = line_text[3:].strip()
                if line_text.endswith('...'):
                    line_text = line_text[:-3].strip()
                if line_text.startswith('-'):
                    line_text = line_text[1:].strip()
                line_text = BeautifulSoup(line_text, 'html.parser').text

                lines.append(SubtitleLine(line_text, entry.start))
                text_buffer.append(line_text)
        full_text = ' '.join(text_buffer)
        return lines, full_text

    def _to_sentences(self, lines, tokens):
        sentences = []
        l, t = 0, 0
        token = None
        line_buffer = []
        line_text = None

        while t < len(tokens):
            if token is None:
                token = tokens[t]
            elif len(token.strip()) == 0:
                sentences.append(Sentence(tokens[t], line_buffer[0].time))
                line_buffer = []
                token = None
                t += 1
                continue

            if line_text is None:
                line_text = lines[l].text
            line_buffer.append(lines[l])

            if token.startswith(line_text):
                token = token[len(line_text):].strip()
                line_text = None
                l += 1
            elif line_text.startswith(token):
                line_text = line_text[len(token):].strip()
                token = ''
            else:
                raise RuntimeError('token/line mismatch')

        return sentences
api/subtitle/opensubtitles.py
class OpenSubtitles:
    """API client to download subtitles from opensubtitles.org"""

    def __init__(self, credentials, client=None):
        """Constructor to prepare API connection.

        :param credentials: username/password tupel
        :param client: optional, custom XMLRPC client
        """
        self.token = None
        self.credentials = credentials
        self.xmlrpc = client or ServerProxy(OPENSUBTITLES_URL, allow_none=True)

    def login(self):
        """Request and save authentication token.

        :raises RuntimeError: if login fails
        """
        username = self.credentials[0]
        password = self.credentials[1]
        resp = self.xmlrpc.LogIn(username, password, LANGUAGE, OPENSUBTITLES_UA)
        self._ensure_success(resp)
        self.token = resp.get('token')

    def find_by_query(self, query):
        """Find subtitles by query.

        Note that it first tries to find and return a local fixture,
        and only does an HTTP call if none was found.

        :param query: query string describing movie
        :returns: list of subtitles that match query
        """
        qry = query.lower().strip()
        resp = self._fixture('query', qry) \
            or self._find({'query': qry, 'sublanguageid': 'eng'})
        return self._resp_to_model(resp)

    def find_subtitles_for_movie(self, imdb_id):
        """Find subtitle by IMDb ID.

        Note that it first tries to find and return a local fixture,
        and only does an HTTP call if none was found.

        :param imdb_id: IMDb ID of movie (starts with 'tt')
        :returns: list of subtitles for movie
        """
        search_id = imdb_id.replace('tt', '').lstrip('0')
        resp = self._fixture('id', imdb_id) \
            or self._find({'imdbid': search_id, 'sublanguageid': 'eng'})
        return self._resp_to_model(resp)

    def load_text(self, subtitle_id, subtitle_encoding):
        """Load subtitle text for movie.

        :param subtitle_id: ID of subtitle
        :param subtitle_encoding: encoding of subtitle text
        :returns: string with movie subtitle text
        """
        resp = self._fixture('subtitle', subtitle_id) \
            or self._download(subtitle_id)

        text = resp.get('data')[0].get('data')
        text = base64.standard_b64decode(text)
        text = zlib.decompress(text, 47)
        text = str(text, subtitle_encoding)
        text = text.lstrip(UNICODE_BOM)
        text = re.sub(NEWLINE_PATTERN, '\n', text)

        return text

    @retry(stop_max_delay=5000, stop_max_attempt_number=3)
    def _download(self, subtitle_id):
        if not self.token:
            self.login()

        resp = self.xmlrpc.DownloadSubtitles(self.token, [subtitle_id])
        self._ensure_success(resp)
        return resp

    @retry(stop_max_delay=5000, stop_max_attempt_number=3)
    def _find(self, query):
        if not self.token:
            self.login()

        resp = self.xmlrpc.SearchSubtitles(self.token, [query], [{'limit': 500}])
        self._ensure_success(resp)
        return resp

    def _ensure_success(self, resp):
        if resp.get('status').split()[0] != '200':
            raise RuntimeError("received status {}".format(resp.get('status')))

    def _fixture(self, directory, arg):
        data = load_fixture(os.path.join('opensubtitles', directory, arg + '.json'))
        if data:
            return json.loads(data)

    def _resp_to_model(self, resp):
        return [to_model(item) for item in resp.get('data')]
web/routes/error.py
def error():
    return render_template('error.html')
domain/search.py
class Searcher:
    """Find movie by query."""

    def __init__(self, subtitle_api, poster_api):
        self.subtitle_api = subtitle_api
        self.poster_api = poster_api

    def search(self, query, limit=10):
        """Find movie by query.

        :param query: query for movie search
        :param limit: limit of results
        :return: list of movies
        """
        subtitles = self.subtitle_api.find_by_query(query)

        movie_downloads = Counter()
        movie_by_id = {}
        for s in subtitles:
            if s.media.kind == 'movie':
                media_id = s.media.id
                movie_by_id[media_id] = s.media
                movie_downloads[media_id] += s.downloads

        most_popular_movies = [movie_by_id[m[0]] for m in movie_downloads.most_common(limit)]
        self._add_posters(most_popular_movies)
        return most_popular_movies

    def _add_posters(self, movies):
        posters = self.poster_api.get_movie_posters([m.id for m in movies])
        for movie in movies:
            movie.poster_url = posters.get(movie.id, None)
api/poster/fanart.py
class FanArt:
    """API client to find posters for movies on fanart.tv"""

    def __init__(self, api_key, client=Fetcher(FANART_URL)):
        """Constructor to prepare API connection.

        :param api_key: valid API key for fanart.tv
        :param fetcher: client for making a batch of HTTP GET requests
        """
        self.api_key = api_key
        self.client = client

    def get_movie_posters(self, imdb_ids):
        """
        For each movie identified by ID, find a poster URL.

        :param imdb_ids: list of IMDb IDs
        :returns: dictionary of poster URLs by IMDb ID
        """
        def to_url(id):
            query = urlencode({'api_key': self.api_key})
            return '/movies/{}?{}'.format(id, query)

        responses = self.client.get([to_url(id) for id in imdb_ids])
        responses_data = [r.json() for r in responses if r and r.status_code == 200]

        poster_url_by_id = {}
        for response_data in responses_data:
            poster_data = response_data.get('movieposter', None)
            if not poster_data:
                continue

            id = response_data['imdb_id']
            poster_url_by_id[id] = poster_data[0]['url'] \
                .replace('/fanart', '/preview') \
                .replace('http:', 'https:')
        return poster_url_by_id
domain/corpus.py
class Corpus:
    """Database of word frequency in the English language."""

    def __init__(self, filepath):
        if filepath in CACHE:
            self.freq_lookup = CACHE[filepath]
        else:
            self.freq_lookup = {}
            with open(filepath, 'r') as file:
                for line in file:
                    word, freq = line.split(' ')
                    self.freq_lookup[word] = int(freq)
            CACHE[filepath] = self.freq_lookup

    def freq(self, word):
        return self.freq_lookup.get(word, 0)

    @staticmethod
    def to_difficulty(freq):
        """Convert the frequency of a word into a difficulty rating.

        :param freq: a word's frequency in the English language
        :return: difficulty rating
        """
        if freq <= 1000:
            return WordDifficulty.HARD
        elif freq <= 5000:
            return WordDifficulty.MED
        elif freq <= 10000:
            return WordDifficulty.EASY
        return WordDifficulty.BASIC


Question: import os
import jinja2
from flask_json import as_json
from web.routes.api_analysis import analysis_api
from web.routes.api_search import search_api
from web.routes.api_word import words_api
from web.routes.bootstrap import bootstrap
from web.routes.error import error
from api.dictionary.wordnik import Wordnik
from api.subtitle.opensubtitles import OpenSubtitles
from api.poster.fanart import FanArt
from domain.analyse import Analyser
from domain.corpus import Corpus, CorpusDatabase
from domain.load import Loader
from domain.parse import Parser
from domain.search import Searcher






TEMPLATE_ROOT = 'templates'


def create_routes(app):
    app.jinja_loader = jinja2.FileSystemLoader(os.path.join(os.getcwd(), TEMPLATE_ROOT))

    opensubtitle_credentials = (app.config['OPENSUBTITLES_USER'], app.config['OPENSUBTITLES_PASS'])
    subtitle_api = OpenSubtitles(opensubtitle_credentials)

    fanart_key = app.config['FANART_TV_KEY']
    poster_api = FanArt(fanart_key)

    wordnik_key = app.config['WORDNIK_KEY']
    wordnik_api = Wordnik(wordnik_key)

    searcher = Searcher(subtitle_api, poster_api)

    corpus = Corpus(CorpusDatabase.FULL)
    loader = Loader(subtitle_api)
    parser = Parser()
    analyser = Analyser(loader, parser, corpus)

    @app.route('/')
    def home_route():
        return bootstrap()

    @app.route('/m/<id>')
    def analysis_route(id):
        return bootstrap()

    @app.route('/m/<id>/w/<word>')
    def word_route(id, word):
        return bootstrap()

    @app.route('/error')
    def error_route():
        return error()

    @app.route('/api/search/<query>')
    @as_json
    def search_api_route(query):
        return search_api(searcher, query)

    @app.route('/api/analysis/<id>')
    @as_json
    def analysis_api_route(id):
        return analysis_api(analyser, poster_api, id)

    @app.route('/api/words/<token>')
    @as_json
    def words_api_route(token):

Answer:         return words_api(wordnik_api, token)