Python wrapper class around HTTP API I wrote this little class to wrap around a work-in-progress, poorly documented API and I'd like to know what else can be improved. I feel like there's a lot of repetition in the functions and I'd love to know if I could turn all those functions (which do the same thing, just setting a different command in the request object) into just one.

import json
import requests

BASE_URL = "https://chaos.aa.net.uk/"

class Chaos(object):
    """This class allows access to the Andrews &amp; Arnold API.

    Note that it is based on trial and error, there is very little
    official documentation about this API yet, so use at your own risk.
    """

    def __init__(self, username, password):
        """Initialize the class.

        Use the same credentials as on control.aa.net.uk.

        Args:
            username: username like xx00@x
            password: self-explanatory
        """
        self.session = requests.session()
        self.session.headers["User-Agent"] = "Python Chaos Client"
        self.session.auth = (username, password)

    def _request(self, **kwargs):
        """Make an API request, lets Requests check the HTTP status code
        then checks if the "error" string is present in the response
        and raises an exception if that's the case.

        Args:
            **kwargs: will be passed as-is to python-requests
        Returns:
            a dict representation of the APi'S JSON reply
        Raises:
            Exception: the remote server returned an error
        """

        resp = self.session.post(BASE_URL, **kwargs)

        if resp.status_code != requests.codes.ok:
            resp.raise_for_status()

        resp = resp.json()

        if "error" in resp:
            raise APIError(resp["error"])

        return resp

    def info(self, **kwargs):
        return self._request(json={**kwargs, **{"command": "info"}})

    def change(self, **kwargs):
        required = ["broadband", "sim", "voip"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={**kwargs, **{"command": "change"}})

    def check(self, **kwargs):
        required = ["order"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={**kwargs, **{"command": "check"}})

    def preorder(self, **kwargs):
        required = ["order"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={**kwargs, **{"command": "preorder"}})

    def order(self, **kwargs):
        required = ["order"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={**kwargs, **{"command": "order"}})

    def usage(self, **kwargs):
        required =  ["broadband", "sim", "voip"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(allowed))

        return self._request(json={**kwargs, **{"command": "usage"}})

    def availability(self, **kwargs):
        required = ["broadband"]

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={**kwargs, **{"command": "availability"}})

class ChaosException(Exception):
    """Base class for all our exceptions.
    """
    pass

class InvalidParameters(ChaosException):
    """Indicates a problem with the request's arguments.
    """
    pass

class APIError(ChaosException):
    """Indicates an error returned by the remote server.
    """
    pass
You're right, you can cut down on the repetition of code. Although bundling all of the functions into one like you mention would work, you lose a bit of the readability that keeping them as class methods gives. Instead, you could just have an internal _command method that collects all of the repeated code:

def _command(self, command_name, required, **kwargs):
    if not any(arg in required for arg in kwargs):
        raise InvalidParameters("Missing object of types: " + ", ".join(required))
    return self._request(json={kwargs, {"command": command_name }})


Note that I've corrected a syntax error in the last line: your parameter used to read json={**kwargs, **{"command": command_name }}.

This makes your Chaos class a lot shorter:

class Chaos(object):
    """This class allows access to the Andrews &amp; Arnold API.

    Note that it is based on trial and error, there is very little
    official documentation about this API yet, so use at your own risk.
    """

    def __init__(self, username, password):
        """Initialize the class.

        Use the same credentials as on control.aa.net.uk.

        Args:
            username: username like xx00@x
            password: self-explanatory
        """
        self.session = requests.session()
        self.session.headers["User-Agent"] = "Python Chaos Client"
        self.session.auth = (username, password)

    def _request(self, **kwargs):
        """Make an API request, lets Requests check the HTTP status code
        then checks if the "error" string is present in the response
        and raises an exception if that's the case.

        Args:
            **kwargs: will be passed as-is to python-requests
        Returns:
            a dict representation of the APi'S JSON reply
        Raises:
            Exception: the remote server returned an error
        """

        resp = self.session.post(BASE_URL, **kwargs)

        if resp.status_code != requests.codes.ok:
            resp.raise_for_status()

        resp = resp.json()

        if "error" in resp:
            raise APIError(resp["error"])

        return resp

    def _command(self, command_name, required, **kwargs):
        """Make an API request, checking that the arguments are valid

        Args:
            command_name: the command name to be passed to the API
            required: a list of names of required arguments
            **kwargs: will be passed as-is to python-requests
        Returns:
            a dict representation of the APi'S JSON reply
        Raises:
            InvalidParameters: the wrong set of arguments was given
        """

        if not any(arg in required for arg in kwargs):
            raise InvalidParameters("Missing object of types: " + ", ".join(required))

        return self._request(json={kwargs, {"command": command_name }})


    def info(self, **kwargs):
        return self._command('info', [], **kwargs)

    def change(self, **kwargs):        
        return self._command('change', ["broadband", "sim", "voip"], **kwargs)

    def check(self, **kwargs):
        return self._command('check', ["order"], **kwargs)

    def preorder(self, **kwargs):
        return self._command('preorder', ["order"], **kwargs)

    def order(self, **kwargs):
        return self._command('order', ["order"], **kwargs)

    def usage(self, **kwargs):
        return self._command('usage', ["broadband", "sim", "voip"], **kwargs)        

    def availability(self, **kwargs):
        return self._command('usage', ["broadband"], **kwargs)


Since _request only ever gets called by _command you could compress this further by making them one function, but it's fine as is. 

As a side note, this code only checks that the minimum set of arguments is given. Depending on your API, it might be a problem if you give extra arguments that aren't required or supported.