From 4ee1fef63f4577c1603159d11fee161caf548153 Mon Sep 17 00:00:00 2001 From: Rossen Georgiev Date: Sun, 29 May 2016 19:35:15 +0100 Subject: [PATCH] WebAPI: apihost param; get/post shortcuts * tweaks to WebAPI user guide section * can now specify hostname for API * added post/get function as shortcuts for calling endpoints --- docs/api/steam.webapi.rst | 4 - docs/user_guide.rst | 25 ++-- steam/webapi.py | 257 +++++++++++++++++++++++++------------- tests/test_webapi.py | 24 +++- 4 files changed, 208 insertions(+), 102 deletions(-) diff --git a/docs/api/steam.webapi.rst b/docs/api/steam.webapi.rst index 5a9e63a..d733441 100644 --- a/docs/api/steam.webapi.rst +++ b/docs/api/steam.webapi.rst @@ -3,7 +3,3 @@ webapi .. automodule:: steam.webapi :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/user_guide.rst b/docs/user_guide.rst index f007904..12f505b 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -10,14 +10,14 @@ overview of the functionality available in the ``steam`` module. SteamID ======= -:mod:`SteamID ` can be used to convert the universal steam id +:mod:`SteamID ` can be used to convert the universal steam id to its' various representations. .. note:: - ``SteamID`` is immutable as it inherits from ``int``. + :class:`SteamID ` is immutable as it inherits from :class:`int`. -Example usage -------------- +Converting between representations +---------------------------------- .. code:: python @@ -54,10 +54,10 @@ Example usage 'https://steamcommunity.com/gid/103582791429521412' -Resolving community urls to ``SteamID`` ------------------------------------------ +Resolving community urls to :class:`SteamID ` +-------------------------------------------------------------------- -The ``steamid`` submodule provides function to resolve community urls. +The :mod:`steam.steamid` submodule provides function to resolve community urls. Here are some examples: .. code:: python @@ -76,12 +76,19 @@ WebAPI :mod:`WebAPI ` is a thin Wrapper around `Steam Web API`_. Requires `API Key`_. Upon initialization the instance will fetch all available interfaces and populate the namespace. +Obtaining a key +--------------- + +Any steam user can get a key by visiting http://steamcommunity.com/dev/apikey. +The only requirement is that the user has verified their email. +Then the key can be used on the ``public`` WebAPI. See :class:`steam.webapi.SERVICE` + .. note:: Interface availability depends on the ``key``. Unless the schema is loaded manually. -Example usage -------------- +Calling an endpoint +------------------- .. code:: python diff --git a/steam/webapi.py b/steam/webapi.py index fa51719..3497e65 100644 --- a/steam/webapi.py +++ b/steam/webapi.py @@ -1,14 +1,23 @@ """ WebAPI provides a thin wrapper over `Steam's Web API `_ -Calling an endpoint +It is very fiendly to exploration and prototyping when using ``ipython``, ``notebooks`` or similar. +The ``key`` will determine what WebAPI interfaces and methods are available. + +.. note:: + Some endpoints don't require a key + +Currently the WebAPI can be accessed via one of two API hosts. See :class:`APIHost`. + +Example code: .. code:: python >>> api = WebAPI(key) - >>> api.ISteamUser.ResolveVanityURL(vanityurl="valve", url_type=2) >>> api.call('ISteamUser.ResolveVanityURL', vanityurl="valve", url_type=2) - {u'response': {u'steamid': u'103582791429521412', u'success': 1}} + >>> api.ISteamUser.ResolveVanityURL(vanityurl="valve", url_type=2) + >>> api.ISteamUser.ResolveVanityURL_v1(vanityurl="valve", url_type=2) + {'response': {'steamid': '103582791429521412', 'success': 1}} All globals params (``key``, ``https``, ``format``, ``raw``) can be specified on per call basis. @@ -21,11 +30,23 @@ All globals params (``key``, ``https``, ``format``, ``raw``) can be specified on "success" "1" } """ -import json -from steam.util.web import make_requests_session +import json as _json +from steam.util.web import make_requests_session as _make_session + +class APIHost(object): + """Enum of currently available API hosts.""" + Public = 'api.steampowered.com' + """ available over HTTP (port 80) and HTTPS (port 443)""" + Partner = 'partner.steam-api.com' + """available over HTTPS (port 443) only + + .. note:: + Key is required for every request. If not supplied you will get HTTP 403. + """ DEFAULT_PARAMS = { # api parameters + 'apihost': APIHost.Public, 'key': None, 'format': 'json', # internal @@ -35,80 +56,11 @@ DEFAULT_PARAMS = { } -def webapi_request(path, method='GET', caller=None, params={}, session=None): - """ - Low level function for calling Steam's WebAPI - - :param path: request url - :type path: :class:`str` - :param method: HTTP method (GET or POST) - :type method: :class:`str` - :param caller: caller reference, caller.last_response is set to the last response - :param params: dict of WebAPI and endpoint specific params - :type params: :class:`dict` - :param session: an instance requests session, or one is created per call - :type session: :class:`requests.Session` - :return: response based on paramers - :rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str` - """ - if method not in ('GET', 'POST'): - raise NotImplemented("HTTP method: %s" % repr(self.method)) - - onetime = {} - for param in DEFAULT_PARAMS: - params[param] = onetime[param] = params.get(param, - DEFAULT_PARAMS[param], - ) - path = "%s://api.steampowered.com/%s" % ('https' if params.get('https', True) else 'http', - path) - del params['raw'] - del params['https'] - del params['http_timeout'] - - if onetime['format'] not in ('json', 'vdf', 'xml'): - raise ValueError("Expected format to be json,vdf or xml; got %s" % onetime['format']) - - # serialize some parameter types properly - for k, v in params.items(): - if isinstance(v, bool): - params[k] = 1 if v else 0 - elif isinstance(v, (list, dict)): - params[k] = json.dumps(v) - - # move params to data, if data is not specified for POST - # simplifies code calling this method - kwargs = {'params': params} if method == "GET" else {'data': params} - - f = getattr(session, method.lower()) - resp = f(path, stream=False, timeout=onetime['http_timeout'], **kwargs) - - # we keep a reference of the last response instance on the caller - if caller is not None: - caller.last_response = resp - - # 4XX and 5XX will cause this to raise - resp.raise_for_status() - - # response - if onetime['raw']: - return resp.text - - if onetime['format'] == 'json': - return resp.json() - elif onetime['format'] == 'xml': - import lxml.etree - return lxml.etree.fromstring(resp.content) - elif onetime['format'] == 'vdf': - import vdf - return vdf.loads(resp.text) - - class WebAPI(object): - """ - Steam WebAPI wrapper. See https://developer.valvesoftware.com/wiki/Steam_Web_API + """Steam WebAPI wrapper .. note:: - Interfaces and methods are populated automatically from WebAPI. + Interfaces and methods are populated automatically from Steam WebAPI. :param key: api key from https://steamcommunity.com/dev/apikey :type key: :class:`str` @@ -120,7 +72,9 @@ class WebAPI(object): :type https: :class:`bool` :param http_timeout: HTTP timeout in seconds :type http_timeout: :class:`int` - :param auto_load_interfaces: load interfaces from the WebAPI + :param apihost: api hostname, see :class:`APIHost` + :type apihost: :class:`str` + :param auto_load_interfaces: load interfaces from the Steam WebAPI :type auto_load_interfaces: :class:`bool` These can be specified per method call for one off calls @@ -130,20 +84,23 @@ class WebAPI(object): raw = DEFAULT_PARAMS['raw'] https = DEFAULT_PARAMS['https'] http_timeout = DEFAULT_PARAMS['http_timeout'] + apihost = DEFAULT_PARAMS['apihost'] interfaces = [] def __init__(self, key, format = DEFAULT_PARAMS['format'], raw = DEFAULT_PARAMS['raw'], https = DEFAULT_PARAMS['https'], http_timeout = DEFAULT_PARAMS['http_timeout'], + apihost = DEFAULT_PARAMS['apihost'], auto_load_interfaces = True): self.key = key #: api key self.format = format #: format (``json``, ``vdf``, or ``xml``) self.raw = raw #: return raw reponse or parse self.https = https #: use https or not self.http_timeout = http_timeout #: HTTP timeout in seconds + self.apihost = apihost #: ..versionadded:: 0.8.3 apihost hostname self.interfaces = [] #: list of all interfaces - self.session = make_requests_session() #: :class:`requests.Session` from :func:`steam.util.web.make_requests_session` + self.session = _make_session() #: :class:`requests.Session` from :func:`steam.util.web.make_requests_session` if auto_load_interfaces: self.load_interfaces(self.fetch_interfaces()) @@ -163,14 +120,14 @@ class WebAPI(object): The returned value can passed to :py:func:`WebAPI.load_interfaces` """ - return webapi_request( - "ISteamWebAPIUtil/GetSupportedAPIList/v1/", - method="GET", + return get('ISteamWebAPIUtil', 'GetSupportedAPIList', 1, + https=self.https, + apihost=self.apihost, caller=None, + session=self.session, params={'format': 'json', 'key': self.key, }, - session=self.session, ) def load_interfaces(self, interfaces_dict): @@ -213,7 +170,7 @@ class WebAPI(object): def doc(self): """ :return: Documentation for all interfaces and their methods - :rtype: :class:`str` + :rtype: str """ doc = "Steam Web API - List of all interfaces\n\n" for interface in self.interfaces: @@ -257,6 +214,10 @@ class WebAPIInterface(object): def key(self): return self._parent.key + @property + def apihost(self): + return self._parent.apihost + @property def https(self): return self._parent.https @@ -278,6 +239,10 @@ class WebAPIInterface(object): return self._parent.session def doc(self): + """ + :return: Documentation for all methods on this interface + :rtype: str + """ return self.__doc__ @property @@ -360,13 +325,20 @@ class WebAPIMethod(object): else: params[name] = kwargs[name] - # make the request + url = "%s://%s/%s/%s/v%s/" % ( + 'https' if self._parent.https else 'http', + self._parent.apihost, + self._parent.name, + self.name, + self.version, + ) + return webapi_request( - "%s/%s/v%s/" % (self._parent.name, self.name, self.version), + url=url, method=self.method, caller=self, - params=params, session=self._parent.session, + params=params, ) @property @@ -386,6 +358,10 @@ class WebAPIMethod(object): return self._dict['name'] def doc(self): + """ + :return: Documentation for this method + :rtype: str + """ return self.__doc__ @property @@ -411,3 +387,112 @@ class WebAPIMethod(object): ) return doc + +def webapi_request(url, method='GET', caller=None, session=None, params=None): + """Low level function for calling Steam's WebAPI + + .. versionchanged:: 0.8.3 + + :param url: request url (e.g. ``https://api.steampowered.com/A/B/v001/``) + :type url: :class:`str` + :param method: HTTP method (GET or POST) + :type method: :class:`str` + :param caller: caller reference, caller.last_response is set to the last response + :param params: dict of WebAPI and endpoint specific params + :type params: :class:`dict` + :param session: an instance requests session, or one is created per call + :type session: :class:`requests.Session` + :return: response based on paramers + :rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str` + """ + if method not in ('GET', 'POST'): + raise NotImplemented("HTTP method: %s" % repr(self.method)) + if params is None: + params = {} + + onetime = {} + for param in DEFAULT_PARAMS: + params[param] = onetime[param] = params.get(param, DEFAULT_PARAMS[param]) + map(params.pop, ('raw', 'apihost', 'https', 'http_timeout')) + + if onetime['format'] not in ('json', 'vdf', 'xml'): + raise ValueError("Expected format to be json,vdf or xml; got %s" % onetime['format']) + + for k, v in params.items(): # serialize some types + if isinstance(v, bool): params[k] = 1 if v else 0 + elif isinstance(v, (list, dict)): params[k] = _json.dumps(v) + + kwargs = {'params': params} if method == "GET" else {'data': params} # params to data for POST + + if session is None: session = _make_session() + + f = getattr(session, method.lower()) + resp = f(url, stream=False, timeout=onetime['http_timeout'], **kwargs) + + # we keep a reference of the last response instance on the caller + if caller is not None: caller.last_response = resp + # 4XX and 5XX will cause this to raise + resp.raise_for_status() + + if onetime['raw']: + return resp.text + elif onetime['format'] == 'json': + return resp.json() + elif onetime['format'] == 'xml': + from lxml import etree as _etree + return _etree.fromstring(resp.content) + elif onetime['format'] == 'vdf': + import vdf as _vdf + return _vdf.loads(resp.text) + +def get(interface, method, version=1, + apihost=DEFAULT_PARAMS['apihost'], https=DEFAULT_PARAMS['https'], + caller=None, session=None, params=None): + """Send GET request to an API endpoint + + .. versionadded:: 0.8.3 + + :param interface: interface name + :type interface: str + :param method: method name + :type method: str + :param version: method version + :type version: int + :param apihost: API hostname + :type apihost: str + :param https: whether to use HTTPS + :type https: bool + :param params: parameters for endpoint + :type params: dict + :return: endpoint response + :rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str` + """ + url = u"%s://%s/%s/%s/v%s/" % ( + 'https' if https else 'http', apihost, interface, method, version) + return webapi_request(url, 'GET', caller=caller, session=session, params=params) + +def post(interface, method, version=1, + apihost=DEFAULT_PARAMS['apihost'], https=DEFAULT_PARAMS['https'], + caller=None, session=None, params=None): + """Send POST request to an API endpoint + + .. versionadded:: 0.8.3 + + :param interface: interface name + :type interface: str + :param method: method name + :type method: str + :param version: method version + :type version: int + :param apihost: API hostname + :type apihost: str + :param https: whether to use HTTPS + :type https: bool + :param params: parameters for endpoint + :type params: dict + :return: endpoint response + :rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str` + """ + url = "%s://%s/%s/%s/v%s/" % ( + 'https' if https else 'http', apihost, interface, method, version) + return webapi_request(url, 'POST', caller=caller, session=session, params=params) diff --git a/tests/test_webapi.py b/tests/test_webapi.py index 8868210..6890cbb 100644 --- a/tests/test_webapi.py +++ b/tests/test_webapi.py @@ -2,13 +2,14 @@ import unittest import mock import vcr +from steam import webapi from steam.webapi import WebAPI from steam.enums import EType, EUniverse test_api_key = 'test_api_key' test_vcr = vcr.VCR( - record_mode='new_episodes', + record_mode='none', # change to 'new_episodes' when recording serializer='yaml', filter_query_parameters=['key'], filter_post_data_parameters=['key'], @@ -26,7 +27,7 @@ class TCwebapi(unittest.TestCase): @test_vcr.use_cassette('webapi.yaml') def test_simple_api_call(self): - resp = self.api.ISteamWebAPIUtil.GetServerInfo() + resp = self.api.ISteamWebAPIUtil.GetServerInfo_v1() self.assertTrue('servertime' in resp) @test_vcr.use_cassette('webapi.yaml') @@ -44,5 +45,22 @@ class TCwebapi(unittest.TestCase): resp = self.api.ISteamRemoteStorage.GetPublishedFileDetails(itemcount=5, publishedfileids=[1,1,1,1,1]) self.assertEqual(resp['response']['resultcount'], 5) - resp = self.api.ISteamUser.ResolveVanityURL(vanityurl='valve', url_type=2) + @test_vcr.use_cassette('webapi.yaml') + def test_get(self): + resp = webapi.get('ISteamUser', 'ResolveVanityURL', 1, + session=self.api.session, params={ + 'key': test_api_key, + 'vanityurl': 'valve', + 'url_type': 2, + }) self.assertEqual(resp['response']['steamid'], '103582791429521412') + + @test_vcr.use_cassette('webapi.yaml') + def test_post(self): + resp = webapi.post('ISteamRemoteStorage', 'GetPublishedFileDetails', 1, + session=self.api.session, params={ + 'key': test_api_key, + 'itemcount': 5, + 'publishedfileids': [1,1,1,1,1], + }) + self.assertEqual(resp['response']['resultcount'], 5)