1
\$\begingroup\$

I've written a program that gets the web API endpoint for some functions on a website. I'm just wondering if you guys see anything I could do to improve the code at all.

import sys
import requests
import dateutil

class KenoBase:
    '''
    Methods that are to be inherited to 'KenoAPI' class.
    Do not use this class, instead use 'KenoAPI''.
    '''

    def __init__(self, state="NT"):
        self.__state__ = state.upper()
        self.__states__ = ["ACT", "NSW", "QLD", "VIC", "WA", "NT", "SA", "TAS"]
        self.__base_url__ = "https://api-info-{}.keno.com.au".format(
                                     self.__state_redirect__.lower())

    def __get_url__(self, end_point="", additional_params=""):
        '''
        Private Method:
        concatenates the base URL and the endpoint together a
        long with additional parameters
        '''
        end_point = str(end_point)
        params = "?jurisdiction={}".format(
        self.__state_redirect__) + additional_params
        complete_url = self.__base_url__ + end_point + params

        return str(complete_url)

    @property
    def __state_redirect__(self):
        '''
        Private Method:
        redirects user input
        '''
        if any(x == self.__state__ for x in self.__states__) is False:
            return sys.exit(str("Check state input: '{}' - is invalid").format(
                                                  self.__state__))

        if self.__state__ == self.__states__[4]:
            print("Keno is not available in WA-Automatically changed to NSW")
            self.__state__ = self.__states__[2]
            return self.__state__

        redirect = [self.__states__[5], self.__states__[6], self.__states__[7]]

        if any(x == self.__state__ for x in redirect) is True:
            print(str("Keno is not available in '{}', this state uses ACT ").format(
self.__state__))
            self.__state__ = self.__states__[0]
            return self.__state__

        return self.__state__

    # noinspection PyDefaultArgument
    @staticmethod
    def __nested_dict__(key=dict, additional_key=""):
        '''
        Private Method:    
        this function speeds up the lookup times for nested dictionaries
        '''
        return key.get(additional_key)

    def __transform_time__(self, _datetime):
        pass
        '''
        Private Method:
        Transforms a date in a datetime object with the correct 
        time information, it also factors in daylight savings
        
        currently working on adding tz and dst to this function
        '''
        return dateutil.parser.isoparse(_datetime).strftime("%Y-%m-%d %H:%M:%S.%f")

    def __results_selection__(self, initial_draw=1, 
      total_draws=1, start_date="2021-02-08", page_size=1, page_number=1):
        '''
        Private Method:
            
            initial_draw: the first game number
            total_draws: how many games you want to select
            start_date: which date you would like to get the games from
            page_size: how many games are on each page
            page_number: if your page size is less than the total draws the games will be split among multiple pages
        
            Min adn Max values for a each parameter 
            game_number: Min: 0, Max: 999
            
            Number of Games: Min:1, Max:200
            
            page_size: Min:1, Max:200
            
            page_number: Min:1, Max:100
        '''

        url = self.__get_url__(end_point="/v2/info/history", additional_params="&starting_game_number={}&number_of_games={}&date={}&page_size={}&page_number={}").format(
            initial_draw, total_draws, start_date, page_size, page_number)
        return dict(requests.get(url).json())

class KenoAPI(KenoBase):
    '''
    Has all possible URL endpoints.
    To learn more visit the wiki. https://github.com/JGolafshan/KenoAPI/wiki/Keno-API-Functions
    '''

    def __init__(self, state):
        super().__init__(state)

    def game_status(self):
        '''
        Public Method:
            Desc: Retrieves information about the current and next game
        '''
        url = self.__get_url__(end_point="/v2/games/kds", additional_params="")
        retrieved = dict(requests.get(url).json())

        status_current = {
            "starting_time": self.__transform_time__(_datetime=self.__nested_dict__(key=retrieved.get("current"), additional_key="closed")),
            "game_number": self.__nested_dict__(key=retrieved.get("current"), additional_key="game-number")
        }

        status_selling = {
            "starting_time": self.__transform_time__(_datetime=self.__nested_dict__(key=retrieved.get("selling"), additional_key="closing")),
            "game_number": self.__nested_dict__(key=retrieved.get("selling"), additional_key="game-number")
        }

        status = {
            "state": self.__state__,
            "current_game": status_current,
            "next_game": status_selling
        }

        return status

    def live_draw(self):
        '''
        Public Method:
            Desc: Retrieves data from the current draw
        '''
        url = self.__get_url__(end_point="/v2/games/kds", additional_params="")
        retrieved = dict(requests.get(url).json().get("current"))
        status = str(retrieved.get("_type")).split(".")
        status_type = status[-1]

        live_draw = {
            "state": self.__state__,
            "game_number": retrieved.get("game-number"),
            "status": status_type,
            "started_at": self.__transform_time__(_datetime=retrieved.get("closed")),
            "is_finished": None,
            "draw_numbers": retrieved.get("draw"),
            "bonus": self.__nested_dict__(retrieved.get("variants"), additional_key="bonus"),
            "heads": self.__nested_dict__(retrieved.get("variants"), additional_key="heads-or-tails")["heads"],
            "tails": self.__nested_dict__(retrieved.get("variants"), additional_key="heads-or-tails")["tails"],
            "result": self.__nested_dict__(retrieved.get("variants"), additional_key="heads-or-tails")["result"]
        }

        if retrieved.get("_type") == "application/vnd.tabcorp.keno.game.complete":
            live_draw.update({"is_finished": bool(True)})

        else:
            live_draw.update({"is_finished": bool(False)})

        return live_draw

    def jackpot(self):
        '''
        Public Method:
            Desc: Retrieves MegaMillions(leveraged) and Regular jackpots
        '''
        url = self.__get_url__(
            end_point="/v2/info/jackpots", additional_params="")
        retrieved = dict(requests.get(url).json())["jackpots"]

        jackpot_regular = {
            "ten_spot": self.__nested_dict__(key=retrieved.get("ten-spot"), additional_key="base"),
            "nine_spot": self.__nested_dict__(key=retrieved.get("nine-spot"), additional_key="base"),
            "eight_spot": self.__nested_dict__(key=retrieved.get("eight-spot"), additional_key="base"),
            "seven_spot": self.__nested_dict__(key=retrieved.get("seven-spot"), additional_key="base")
        }

        jackpot_leveraged = {
            "ten_spot": self.__nested_dict__(key=retrieved.get("ten-spot-mm"), additional_key="base"),
            "nine_spot": self.__nested_dict__(key=retrieved.get("nine-spot-mm"), additional_key="base"),
            "eight_spot": self.__nested_dict__(key=retrieved.get("eight-spot-mm"), additional_key="base"),
            "seven_spot": self.__nested_dict__(key=retrieved.get("seven-spot-mm"), additional_key="base")
        }

        jackpot_combined = {
            "state": self.__state__,
            "regular": jackpot_regular,
            "leveraged": jackpot_leveraged
        }

        return jackpot_combined

    def hot_cold(self):
        '''
        Public Method:
            Desc: Retrieves trending numbers which are defined the official keno website
        '''
        url = self.__get_url__(
            end_point="/v2/info/hotCold", additional_params="")
        retrieved = dict(requests.get(url).json())

        hot_cold = {
            "cold": retrieved.get("coldNumbers"),
            "hot": retrieved.get("hotNumbers"),
            "last_updated": retrieved.get("secondsSinceLastReceived"),
            "state": self.__state__
        }
        return hot_cold
\$\endgroup\$
4
  • \$\begingroup\$ Can you show how this is invoked? \$\endgroup\$
    – Reinderien
    Jul 9, 2021 at 15:59
  • \$\begingroup\$ Calling KenoAPI('NT').game_status() fails - you're not checking for the success of your get, but it's returning a 403. \$\endgroup\$
    – Reinderien
    Jul 9, 2021 at 16:06
  • \$\begingroup\$ In fact the site seems entirely down, as I get a 403 no matter what URL I visit \$\endgroup\$
    – Reinderien
    Jul 9, 2021 at 16:24
  • \$\begingroup\$ Hey, I'm not too sure what happened to the website, When I posted this everything was working fine. Seems like bad luck that it happened when it did. \$\endgroup\$ Jul 10, 2021 at 1:17

1 Answer 1

3
\$\begingroup\$

Aside the fact that I can't test anything due to the site being down, the code has some issues that could be cleaned up:

  • It's not clear why KenoBase exists - there's only one child, and I don't see this buying you any useful abstraction
  • You need to get out of the habit of __decorating__ your members. Double underscores are reserved for name mangling. Private variables in Python are very much "by convention" instead of being enforced, and use a single leading underscore rather than leading and trailing dunders.
  • The params = "?jurisdiction={}" setup you do is unneeded, and should be rewritten as a params kwarg dict passed into get() rather than being baked into the URL
  • str(complete_url) is redundant; that's already a string
  • if any(...) is False should be if not any(...); and if any(...) is True should be if any(...)
  • Consider replacing your format() calls with interpolated f-strings
  • return sys.exit makes no sense. exit will raise an exception meant to cut through the stack and terminate the program, and so certainly the return will not happen.
  • key=dict is a mystery. You're setting the default of the key argument to be a type. If you allow the default to be set, then key.get() is going to explode.
  • The pass in transform_time can be deleted
  • There is no need to re-cast the result of json() into a dict; it's already a dict

Lines like this:

dict(requests.get(url).json())

should be

with requests.get(url) as response:
    response.raise_for_status()
    retrieved = response.json()

otherwise, when the site fails (as it is now with a 403) your application will not fail as early as it should, and the error will be much less clear.

\$\endgroup\$
4
  • \$\begingroup\$ Hey, thanks for all the useful pointers, regarding the private methods, is there any way I can make them so they can only be used inside the class, for example when KenoAPI("NT") is called I don't want the user to have access to state_redirect \$\endgroup\$ Jul 10, 2021 at 1:23
  • 1
    \$\begingroup\$ Not really, no. Convention is that privates are marked with one underscore, but they can still be accessed. \$\endgroup\$
    – Reinderien
    Jul 10, 2021 at 3:14
  • \$\begingroup\$ I found something regarding this, using '__decorating' hides the function or variable when calling it outside the class. \$\endgroup\$ Jul 10, 2021 at 15:08
  • 1
    \$\begingroup\$ Don't do it. Read stackoverflow.com/questions/7456807/python-name-mangling \$\endgroup\$
    – Reinderien
    Jul 10, 2021 at 15:21

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.