11
\$\begingroup\$

Some days ago, I figured out that I'd like to get a notification every time a new question is asked here. So, I've made a really simple Python script, which uses pync (only OSX compatible) to do that for me.

I'd like to get some feedback regarding anything that comes to your mind, including good practices. That's not a complicated piece of code, but I'd like some feedback on it anyway.

import stackexchange
import time

from pync import Notifier


def get_notifications():
    """This method uses the SE API and pops up a notification everytime a new question is asked on CR"""
    cr = stackexchange.Site(stackexchange.CodeReview)

    old = []
    while True:
        questions = cr.recent_questions(filter='_b')
        for question in questions[:1]:
            if question.title not in old:
                old.append(question.title)
                Notifier.notify('Votes: {0} | Answers: {1} | Views: {2}'.format(
                    question.score, len(question.answers), question.view_count),
                    title='CodeReview: {0}'.format(question.title),
                    subtitle='Tags: {0}'.format(', '.join(question.tags)),
                    sound='default',
                    open=question.url)
            else:
                continue

        time.sleep(2)


if __name__ == '__main__':
    get_notifications()
\$\endgroup\$
8
\$\begingroup\$

Nitpicks

  • Maximum line length should be 72 characters for docstrings.
  • For your old variable, you may wan to use a set instead of a list to check for existence (not in) as it will be \$O(1)\$ instead of \$O(n)\$.

Reusability

As it stand, I can't use this script because it is too much tied to your OSX only notifier. I'd rather yield questions from get_notifications (which will not be greatly named after that) and format them elsewhere, letting other users handle it with other notifications systems (or even email, for that matter).

Something like:

import stackexchange
import time


def pync_notify(question):
    from pync import Notifier
    Notifier.notify('Votes: {0} | Answers: {1} | Views: {2}'.format(
        question.score, len(question.answers), question.view_count),
        title='CodeReview: {0}'.format(question.title),
        subtitle='Tags: {0}'.format(', '.join(question.tags)),
        sound='default', open=question.url)


def gtk_notify(question):
    from gi.repository import Notify
    Notify.init('stackexchange')
    notification = Notify.Notification.new(
        'CodeReview: {}'.format(question.title),
        'Votes: {} | Answers: {} | Views: {}'.format(
            question.score, len(question.answers), question.view_count),
        'dialog-information')
    notification.show()


def get_questions():
    """Fetch recent questions from CodeReview

    This function uses the SE API and yield each
    new question that is asked on CR.
    """

    cr = stackexchange.Site(stackexchange.CodeReview)

    old = {}
    while True:
        questions = cr.recent_questions(filter='_b')
        for question in questions[:1]:
            if question.title not in old:
                old.add(question.title)
                yield question

        time.sleep(2)


if __name__ == '__main__':
    for question in get_questions():
         pync_notify(question)

Should be enough. Note that I import modules each time a new question appears, which is not ideal, but at least it let you easily deal with modules that are not present on the system. And the few extra time spent realizing the module is already loaded is negligible compared to your time.sleep(2) anyway. You may want to use classes with a __call__ method if you really want to import and initialize only once.

On the reusability side, it could also be good to be able to parametrize the generator with the site the user whish to fetch:

def get_questions(site=stackexchange.CodeReview):
    se = stackexchange.Site(site)
    …

Better usage of the stackexchange API

The stackexchange module let you directly query information using "site classes" (such as stackexchange.CodeReview) without necessarily relying on a stackexchange.Site object. So:

CR = stackexchange.CodeReview()
CR.questions()

will be a direct call to the SE API on questions.

From there, two things worth noting:

  1. The sort method;
  2. The fromdate parameter.

Sorting results

The advantage of using sort='creation' in the query parameters is that you only need to store the last asked question in your old variable. You can then iterate over the result of the query and stop when q.title == old.title without storing previous questions anymore.

Asking for questions asked after a given date

The fromdate parameter takes a timestamp and make the query returns only questions asked after that point in time. There is no real need for sorting anymore, all you need to remember is the date at when the last query was made.

Proposed improvements

import stackexchange
import datetime
import time


def pync_notify(question):
    from pync import Notifier
    Notifier.notify('Votes: {0} | Answers: {1} | Views: {2}'.format(
        question.score, len(question.answers), question.view_count),
        title='CodeReview: {0}'.format(question.title),
        subtitle='Tags: {0}'.format(', '.join(question.tags)),
        sound='default', open=question.url)


def gtk_notify(question):
    from gi.repository import Notify
    Notify.init('stackexchange')
    notification = Notify.Notification.new(
        'CodeReview: {}'.format(question.title),
        'Votes: {} | Answers: {} | Views: {}'.format(
            question.score, len(question.answers), question.view_count),
        'dialog-information')
    notification.show()


def now():
    """Return the current timestamp"""
    date = datetime.datetime.now()
    return int(date.timestamp())


def get_questions(site=stackexchange.CodeReview, query_delay=2):
    """Fetch recent questions from the Stack Exchange network

    This function uses the SE API and yield each
    new question that is asked on the given site.
    """

    se = site()
    last_query = now()

    while True:
        fromdate, last_query = last_query, now()
        questions = se.questions(fromdate=fromdate, sort='creation')
        yield from questions
        time.sleep(query_delay)


if __name__ == '__main__':
    for question in get_questions():
         pync_notify(question)

Last minute reading

On the discussion about throttles of the StackExchange API, one can read:

While not strictly a throttle, the Stack Exchange API employs heavy caching and as such no application should make semantically identical requests more than once a minute. This is generally a waste of bandwidth as, more often than not, the exact same result will be returned.

This means that you probably want to increase your time.sleep delay from 2 to at least 60.

\$\endgroup\$
  • \$\begingroup\$ Thanks @MathiasEttinger. It looks awesome. While I'm not a fan of gradually importing the modules this way, I might use this for now. \$\endgroup\$ – Grajdeanu Alex. Sep 28 '16 at 11:34
4
\$\begingroup\$

You don't delete previous items from old and so it keeps increasing. This shouldn't have too much of an impact, but, can eat up your memory after a long while.

The else: continue is also un-needed, as if it's else then it'll go to the beginning of the loop anyway.

\$\endgroup\$
3
\$\begingroup\$

Two more ideas:

  • I would switch the old.append(question.title) and the Notifier.notify-lines. It makes more sense to add the question to the list of already notified questions AFTER they have been (implicit successfully) notified.
  • the indices in string format are redundant if used in order, I would just use Votes: {} | Answers: {} | Views: {}.
\$\endgroup\$
3
\$\begingroup\$

for question in questions[:1]: is just a fancy way for writing for question in questions[0]: which is just question = questions[0].

In addition, I would make old a set. This way the if question.title not in old: check will stay O(1) and not grow with the number of seen questions.

Your format can be slightly simplified by accessing the attribute in the format string:

Notifier.notify('Votes: {0.score} | Answers: {1} | Views: {0.view_count}'.format(
                question, len(question.answers)),
                title='CodeReview: {.title}'.format(question),
                subtitle='Tags: {}'.format(', '.join(question.tags)),
                sound='default',
                open=question.url)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.