From 16567fca0625f5c46a59f18c8ad102a9cb05eb8c Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 16:58:54 -0600 Subject: [PATCH 1/9] Update xkcd.py --- xkcd.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xkcd.py b/xkcd.py index 98ee2bd..534316e 100644 --- a/xkcd.py +++ b/xkcd.py @@ -200,6 +200,10 @@ def __init__(self, number): self.title = xkcdData['safe_title'] self.altText = xkcdData['alt'] self.imageLink = xkcdData['img'] + self.imageNum = xkcdData['num'] + self.day = xkcdData['day'] + self.month = xkcdData['month'] + self.year = xkcdData['year'] # This may no longer be necessary. # if sys.version_info[0] >= 3: @@ -268,6 +272,14 @@ def getExplanation(self): a given comic and returns that URL.""" global explanationUrl return explanationUrl + str(self.number) + + def getImageNumber(self): + """ Returns the Comic number.""" + return self.imageNum + + def getParsedDate(self): + """ Returns Comic date.""" + return self.date def show(self): """ Uses the Python webbrowser module to open the comic in your system's From 1a3cf0e0c36e83272e74ada8384010dee2a824d5 Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 17:09:22 -0600 Subject: [PATCH 2/9] Re-formatted functions Moved functions to before class definitions, allowing using the API to be nicer (now allows the IDE to recognize functions) and for my editor to stop screaming at me --- xkcd.py | 355 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 178 insertions(+), 177 deletions(-) diff --git a/xkcd.py b/xkcd.py index 534316e..4bb989e 100644 --- a/xkcd.py +++ b/xkcd.py @@ -40,6 +40,95 @@ explanationUrl = "https://explainxkcd.com/" # The URL of the explanation. archiveUrl = "https://what-if.xkcd.com/archive/" # The What If Archive URL. +# Functions that work on What Ifs. + +def getWhatIfArchive(): + """ Parses the xkcd What If archive. getWhatIfArchive passes the HTML text of + the archive page into a :class:`WhatIfArchiveParser` and then calls + the parser's :func:`WhatIfArchiveParser.getWhatIfs` method and returns the dictionary produced. + + This function returns a dictionary mapping article numbers to :class:`WhatIf` + objects for every What If article published thus far. If the parsing fails, + for whatever reason, the dictionary will be empty.""" + archive = urllib.urlopen(archiveUrl) + text = archive.read() + if sys.version_info[0] >= 3: + text = text.decode('utf-8') + archive.close() + + parser = WhatIfArchiveParser() + parser.feed(text) + return parser.getWhatIfs() + +def getLatestWhatIfNum(archive=None): + """ Returns an integer representing the number of the latest What If article + published. This is done by calling :class:`getLatestWhatIf` and returning + the number of that method's result. + + Takes an optional "archive" argument. If this argument is None, the + :func:`getWhatIfArchive` routine is first called to populate the archive + of published What If articles. If it is not, however, "archive" is assumed + to be a dictionary and used as the set of articles to chooose from. + """ + + latestWhatIf = getLatestWhatIf(archive) + return latestWhatIf.number + +def getLatestWhatIf(archive=None): + """ Returns a :class:`WhatIf` object representing the latest What If article. + + Takes an optional "archive" argument. If this argument is None, the + :func:`getWhatIfArchive` routine is first called to populate the archive + of published What If articles. If it is not, however, "archive" is assumed + to be a dictionary and used as the set of articles to chooose from. + """ + + if archive is None: + archive = getWhatIfArchive() + + # Get the archive keys as a list and sort them by ascending order. + # The last entry in keys will be the latest What if. + keys = list(archive.keys()) + keys.sort() + return archive[keys[-1]] + +def getRandomWhatIf(): + """ Returns a randomly generated :class:`WhatIf` object, using the Python standard library + random number generator to select the object. The object is returned + from the dictionary produced by :func:`getWhatIfArchive`; like the other What If + routines, this function is called first in order to get a list of all previously + published What Ifs.""" + + random.seed() + archive = getWhatIfArchive() + latest = getLatestWhatIfNum(archive) + number = random.randint(1, latest) + return archive[number] + +def getWhatIf(number): + """ Returns a :class:`WhatIf` object corresponding to the What If article of + index passed to the function. If the index is less than zero or + greater than the maximum number of articles published thus far, + None is returned instead. + + Like all the routines for handling What If articles, :func:`getWhatIfArchive` + is called first in order to establish a list of all previously published + What Ifs. + + Arguments: + + number: an integer or string that represents a number, this is the index of article to retrieve. + + Returns the resulting :class:`WhatIf` object.""" + archive = getWhatIfArchive() + latest = getLatestWhatIfNum(archive) + + if type(number) is str and number.isdigit(): + number = int(number) + if number > latest or latest <= 0: + return None + return archive[number] + class WhatIf: """ @@ -78,6 +167,37 @@ def getLink(self): """Returns a link to the What If article.""" return self.link +# Utility functions + +def convertToAscii(string, error="?"): + """ Utility function that converts a unicode string to ASCII. This + exists so the :class:`Comic` class can be compatible with Python 2 + libraries that expect ASCII strings, such as Twisted (as of this writing, + anyway). It is unlikely something you will need directly, and its + use is discouraged. + + Arguments: + + string: the string to attempt to convert. + + error: a string that will be substituted into 'string' wherever Python is unable + to automatically do the conversion. + + convertToAscii returns the converted string.""" + + running = True + asciiString = string + while running: + try: + asciiString = asciiString.encode('ascii') + except UnicodeError as unicode: + start = unicode.start + end = unicode.end + asciiString = asciiString[:start] + "?" + asciiString[end:] + else: + running = False + return asciiString + # Possibly, BeautifulSoup or MechanicalSoup or something would be nicer # But xkcd currently has no external dependencies and I'd like to keep it that way. class WhatIfArchiveParser(HTMLParser.HTMLParser): @@ -170,6 +290,60 @@ def getWhatIfs(self): If for some reason the parsing has failed, the dictionary will be empty.""" return self.whatifs +# Functions that work on Comics. + +def getLatestComicNum(): + """ Uses the xkcd JSON API to look up the number of the latest xkcd comic. + + Returns that number as an integer.""" + xkcd = urllib.urlopen("https://xkcd.com/info.0.json").read() + xkcdJSON = json.loads(xkcd.decode()) + number = xkcdJSON['num'] + return number + +def getLatestComic(): + """ Produces a :class:`Comic` object for the latest xkcd comic. This function + is just a wrapper around a call to :func:`getLatestComicNum`, and then + constructs a :class:`Comic` object on its return value. + + Returns the resulting comic object.""" + number = getLatestComicNum() + return Comic(number) + +def getRandomComic(): + """ Produces a :class:`Comic` object for a random xkcd comic. Uses the + Python standard library random number generator in order to select + a comic. + + Returns the resulting comic object.""" + random.seed() + numComics = getLatestComicNum() + number = random.randint(1, numComics) + return Comic(number) + +def getComic(number, silent=True): + """ Produces a :class:`Comic` object with index equal to the provided argument. + Prints an error in the event of a failure (i.e. the number is less than zero + or greater than the latest comic number) and returns an empty Comic object. + + Arguments: + an integer or string that represents a number, "number", that is the index of the comic in question. + + silent: boolean, defaults to True. If set to False, an error will be printed + to standard output should the provided integer argument not be valid. + + Returns the resulting Comic object for the provided index if successful, + or a Comic object with -1 as the index if not.""" + numComics = getLatestComicNum() + + if type(number) is str and number.isdigit(): + number = int(number) + if number > numComics or number <= 0: + if not silent: + print("Error: You have requested an invalid comic.") + return Comic(-1) + return Comic(number) + class Comic: """ Class representing a single xkcd comic. These can be produced via number of @@ -201,9 +375,9 @@ def __init__(self, number): self.altText = xkcdData['alt'] self.imageLink = xkcdData['img'] self.imageNum = xkcdData['num'] - self.day = xkcdData['day'] - self.month = xkcdData['month'] - self.year = xkcdData['year'] + self.day = xkcdData['day'] + self.month = xkcdData['month'] + self.year = xkcdData['year'] # This may no longer be necessary. # if sys.version_info[0] >= 3: @@ -327,177 +501,4 @@ def download(self, output="", outputFile="", silent=True): download.write(image) download.close() return output - -# Functions that work on Comics. - -def getLatestComicNum(): - """ Uses the xkcd JSON API to look up the number of the latest xkcd comic. - - Returns that number as an integer.""" - xkcd = urllib.urlopen("https://xkcd.com/info.0.json").read() - xkcdJSON = json.loads(xkcd.decode()) - number = xkcdJSON['num'] - return number - -def getLatestComic(): - """ Produces a :class:`Comic` object for the latest xkcd comic. This function - is just a wrapper around a call to :func:`getLatestComicNum`, and then - constructs a :class:`Comic` object on its return value. - - Returns the resulting comic object.""" - number = getLatestComicNum() - return Comic(number) - -def getRandomComic(): - """ Produces a :class:`Comic` object for a random xkcd comic. Uses the - Python standard library random number generator in order to select - a comic. - - Returns the resulting comic object.""" - random.seed() - numComics = getLatestComicNum() - number = random.randint(1, numComics) - return Comic(number) - -def getComic(number, silent=True): - """ Produces a :class:`Comic` object with index equal to the provided argument. - Prints an error in the event of a failure (i.e. the number is less than zero - or greater than the latest comic number) and returns an empty Comic object. - - Arguments: - an integer or string that represents a number, "number", that is the index of the comic in question. - - silent: boolean, defaults to True. If set to False, an error will be printed - to standard output should the provided integer argument not be valid. - - Returns the resulting Comic object for the provided index if successful, - or a Comic object with -1 as the index if not.""" - numComics = getLatestComicNum() - - if type(number) is str and number.isdigit(): - number = int(number) - if number > numComics or number <= 0: - if not silent: - print("Error: You have requested an invalid comic.") - return Comic(-1) - return Comic(number) - -# Functions that work on What Ifs. - -def getWhatIfArchive(): - """ Parses the xkcd What If archive. getWhatIfArchive passes the HTML text of - the archive page into a :class:`WhatIfArchiveParser` and then calls - the parser's :func:`WhatIfArchiveParser.getWhatIfs` method and returns the dictionary produced. - - This function returns a dictionary mapping article numbers to :class:`WhatIf` - objects for every What If article published thus far. If the parsing fails, - for whatever reason, the dictionary will be empty.""" - archive = urllib.urlopen(archiveUrl) - text = archive.read() - if sys.version_info[0] >= 3: - text = text.decode('utf-8') - archive.close() - - parser = WhatIfArchiveParser() - parser.feed(text) - return parser.getWhatIfs() - -def getLatestWhatIfNum(archive=None): - """ Returns an integer representing the number of the latest What If article - published. This is done by calling :class:`getLatestWhatIf` and returning - the number of that method's result. - - Takes an optional "archive" argument. If this argument is None, the - :func:`getWhatIfArchive` routine is first called to populate the archive - of published What If articles. If it is not, however, "archive" is assumed - to be a dictionary and used as the set of articles to chooose from. - """ - - latestWhatIf = getLatestWhatIf(archive) - return latestWhatIf.number - -def getLatestWhatIf(archive=None): - """ Returns a :class:`WhatIf` object representing the latest What If article. - - Takes an optional "archive" argument. If this argument is None, the - :func:`getWhatIfArchive` routine is first called to populate the archive - of published What If articles. If it is not, however, "archive" is assumed - to be a dictionary and used as the set of articles to chooose from. - """ - - if archive is None: - archive = getWhatIfArchive() - - # Get the archive keys as a list and sort them by ascending order. - # The last entry in keys will be the latest What if. - keys = list(archive.keys()) - keys.sort() - return archive[keys[-1]] - -def getRandomWhatIf(): - """ Returns a randomly generated :class:`WhatIf` object, using the Python standard library - random number generator to select the object. The object is returned - from the dictionary produced by :func:`getWhatIfArchive`; like the other What If - routines, this function is called first in order to get a list of all previously - published What Ifs.""" - - random.seed() - archive = getWhatIfArchive() - latest = getLatestWhatIfNum(archive) - number = random.randint(1, latest) - return archive[number] - -def getWhatIf(number): - """ Returns a :class:`WhatIf` object corresponding to the What If article of - index passed to the function. If the index is less than zero or - greater than the maximum number of articles published thus far, - None is returned instead. - - Like all the routines for handling What If articles, :func:`getWhatIfArchive` - is called first in order to establish a list of all previously published - What Ifs. - - Arguments: - - number: an integer or string that represents a number, this is the index of article to retrieve. - - Returns the resulting :class:`WhatIf` object.""" - archive = getWhatIfArchive() - latest = getLatestWhatIfNum(archive) - - if type(number) is str and number.isdigit(): - number = int(number) - if number > latest or latest <= 0: - return None - return archive[number] - -# Utility functions - -def convertToAscii(string, error="?"): - """ Utility function that converts a unicode string to ASCII. This - exists so the :class:`Comic` class can be compatible with Python 2 - libraries that expect ASCII strings, such as Twisted (as of this writing, - anyway). It is unlikely something you will need directly, and its - use is discouraged. - - Arguments: - - string: the string to attempt to convert. - - error: a string that will be substituted into 'string' wherever Python is unable - to automatically do the conversion. - - convertToAscii returns the converted string.""" - - running = True - asciiString = string - while running: - try: - asciiString = asciiString.encode('ascii') - except UnicodeError as unicode: - start = unicode.start - end = unicode.end - asciiString = asciiString[:start] + "?" + asciiString[end:] - else: - running = False - return asciiString + \ No newline at end of file From 4a03a240b278a4c023a4bf35a6d39e6aaa7a9f8f Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 17:17:27 -0600 Subject: [PATCH 3/9] Added date formatting Options for YMD, DMY, and MDY --- xkcd.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/xkcd.py b/xkcd.py index 4bb989e..4ee3c3a 100644 --- a/xkcd.py +++ b/xkcd.py @@ -380,10 +380,10 @@ def __init__(self, number): self.year = xkcdData['year'] # This may no longer be necessary. -# if sys.version_info[0] >= 3: -# self.title = str(self.title, encoding='UTF-8') -# self.altText = str(self.altText, encoding='UTF-8') -# self.imageLink = str(self.imageLink, encoding='UTF-8') + # if sys.version_info[0] >= 3: + # self.title = str(self.title, encoding='UTF-8') + # self.altText = str(self.altText, encoding='UTF-8') + # self.imageLink = str(self.imageLink, encoding='UTF-8') #Get the image filename offset = len(imageUrl) @@ -451,9 +451,20 @@ def getImageNumber(self): """ Returns the Comic number.""" return self.imageNum - def getParsedDate(self): - """ Returns Comic date.""" - return self.date + # Possible date formats: + # YMD: year, month, day + # DMY: day, month, year + # MDY: month, day, year + def getParsedDate(self, dateType="MDY"): + """ Returns Comic date.""" + if dateType == "MYD": + return "{}/{}/{}".format(self.month, self.year, self.day) + elif dateType == "DMY": + return "{}/{}/{}".format(self.day, self.month, self.year) + elif dateType == "MDY": + return "{}/{}/{}".format(self.month, self.day, self.year) + else: + return "{}/{}/{}".format(self.month, self.day, self.year) def show(self): """ Uses the Python webbrowser module to open the comic in your system's @@ -501,4 +512,3 @@ def download(self, output="", outputFile="", silent=True): download.write(image) download.close() return output - \ No newline at end of file From a09983de4832532273d60e494eadafbd0c8583d8 Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 17:21:49 -0600 Subject: [PATCH 4/9] Converted spaces to Tabs --- xkcd.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/xkcd.py b/xkcd.py index 4ee3c3a..bd91f0e 100644 --- a/xkcd.py +++ b/xkcd.py @@ -375,15 +375,15 @@ def __init__(self, number): self.altText = xkcdData['alt'] self.imageLink = xkcdData['img'] self.imageNum = xkcdData['num'] - self.day = xkcdData['day'] - self.month = xkcdData['month'] - self.year = xkcdData['year'] + self.day = xkcdData['day'] + self.month = xkcdData['month'] + self.year = xkcdData['year'] # This may no longer be necessary. - # if sys.version_info[0] >= 3: - # self.title = str(self.title, encoding='UTF-8') - # self.altText = str(self.altText, encoding='UTF-8') - # self.imageLink = str(self.imageLink, encoding='UTF-8') + # if sys.version_info[0] >= 3: + # self.title = str(self.title, encoding='UTF-8') + # self.altText = str(self.altText, encoding='UTF-8') + # self.imageLink = str(self.imageLink, encoding='UTF-8') #Get the image filename offset = len(imageUrl) @@ -448,23 +448,23 @@ def getExplanation(self): return explanationUrl + str(self.number) def getImageNumber(self): - """ Returns the Comic number.""" - return self.imageNum - - # Possible date formats: - # YMD: year, month, day - # DMY: day, month, year - # MDY: month, day, year - def getParsedDate(self, dateType="MDY"): - """ Returns Comic date.""" - if dateType == "MYD": - return "{}/{}/{}".format(self.month, self.year, self.day) - elif dateType == "DMY": - return "{}/{}/{}".format(self.day, self.month, self.year) - elif dateType == "MDY": - return "{}/{}/{}".format(self.month, self.day, self.year) - else: - return "{}/{}/{}".format(self.month, self.day, self.year) + """ Returns the Comic number.""" + return self.imageNum + + # Possible date formats: + # YMD: year, month, day + # DMY: day, month, year + # MDY: month, day, year + def getParsedDate(self, dateType="MDY"): + """ Returns Comic date.""" + if dateType == "MYD": + return "{}/{}/{}".format(self.month, self.year, self.day) + elif dateType == "DMY": + return "{}/{}/{}".format(self.day, self.month, self.year) + elif dateType == "MDY": + return "{}/{}/{}".format(self.month, self.day, self.year) + else: + return "{}/{}/{}".format(self.month, self.day, self.year) def show(self): """ Uses the Python webbrowser module to open the comic in your system's From 619393d9986c5f3d0e7016ad78a42daff288f6fa Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 17:23:15 -0600 Subject: [PATCH 5/9] Revert --- xkcd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xkcd.py b/xkcd.py index bd91f0e..d77aeca 100644 --- a/xkcd.py +++ b/xkcd.py @@ -380,10 +380,10 @@ def __init__(self, number): self.year = xkcdData['year'] # This may no longer be necessary. - # if sys.version_info[0] >= 3: - # self.title = str(self.title, encoding='UTF-8') - # self.altText = str(self.altText, encoding='UTF-8') - # self.imageLink = str(self.imageLink, encoding='UTF-8') +# if sys.version_info[0] >= 3: +# self.title = str(self.title, encoding='UTF-8') +# self.altText = str(self.altText, encoding='UTF-8') +# self.imageLink = str(self.imageLink, encoding='UTF-8') #Get the image filename offset = len(imageUrl) From dc8b68cf9d0930e951aa5e8ff8b1280af855b0d7 Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Fri, 30 Nov 2018 17:29:09 -0600 Subject: [PATCH 6/9] Re-named --- xkcd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xkcd.py b/xkcd.py index d77aeca..3bfe89d 100644 --- a/xkcd.py +++ b/xkcd.py @@ -447,15 +447,15 @@ def getExplanation(self): global explanationUrl return explanationUrl + str(self.number) - def getImageNumber(self): - """ Returns the Comic number.""" - return self.imageNum + def getNumber(self): + """ Returns the Comic number.""" + return self.imageNum # Possible date formats: # YMD: year, month, day # DMY: day, month, year # MDY: month, day, year - def getParsedDate(self, dateType="MDY"): + def getDate(self, dateType="MDY"): """ Returns Comic date.""" if dateType == "MYD": return "{}/{}/{}".format(self.month, self.year, self.day) From 3287f77fcbc7b27ddad131396caba02075e0f644 Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Sat, 1 Dec 2018 07:57:41 -0600 Subject: [PATCH 7/9] Converted to datetime --- xkcd.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/xkcd.py b/xkcd.py index 3bfe89d..1d944b4 100644 --- a/xkcd.py +++ b/xkcd.py @@ -24,6 +24,7 @@ import random import sys import webbrowser +import datetime # Python 3 support! if sys.version_info[0] <= 2: @@ -451,20 +452,11 @@ def getNumber(self): """ Returns the Comic number.""" return self.imageNum - # Possible date formats: - # YMD: year, month, day - # DMY: day, month, year - # MDY: month, day, year - def getDate(self, dateType="MDY"): - """ Returns Comic date.""" - if dateType == "MYD": - return "{}/{}/{}".format(self.month, self.year, self.day) - elif dateType == "DMY": - return "{}/{}/{}".format(self.day, self.month, self.year) - elif dateType == "MDY": - return "{}/{}/{}".format(self.month, self.day, self.year) - else: - return "{}/{}/{}".format(self.month, self.day, self.year) + def getDate(self): + """ Returns datetime object of the Comic's date.""" + rawDate = "{}/{}/{}".format(self.month, self.day, self.year) + date = datetime.datetime.strptime(rawDate, "%m/%d/%Y") + return date def show(self): """ Uses the Python webbrowser module to open the comic in your system's From 633bd05e6cc41d2445f88db9e85ccd8cf80a083a Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Sat, 1 Dec 2018 09:06:00 -0600 Subject: [PATCH 8/9] Added getRawJson for flexibility for developers --- xkcd.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xkcd.py b/xkcd.py index 1d944b4..377bf14 100644 --- a/xkcd.py +++ b/xkcd.py @@ -372,6 +372,7 @@ def __init__(self, number): jsonString = self.link + "/info.0.json" xkcd = urllib.urlopen(jsonString).read() xkcdData = json.loads(xkcd.decode()) + self.json = urllib.urlopen(jsonString) self.title = xkcdData['safe_title'] self.altText = xkcdData['alt'] self.imageLink = xkcdData['img'] @@ -458,6 +459,10 @@ def getDate(self): date = datetime.datetime.strptime(rawDate, "%m/%d/%Y") return date + def getRawJson(self): + """ Returns the raw json file from the xkcd api """ + return self.json + def show(self): """ Uses the Python webbrowser module to open the comic in your system's web browser.""" From fd547d2c24fbc21f889d840296209f02ceac8129 Mon Sep 17 00:00:00 2001 From: Kixiron <25047011+Kixiron@users.noreply.github.com> Date: Sat, 1 Dec 2018 09:18:09 -0600 Subject: [PATCH 9/9] Ignored settings.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3af8207..dae4edd 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ coverage.xml # Sphinx documentation docs/_build/ docs/build/ +.vscode/settings.json