Browse Source

move UMs and Leaderboards into seperate files

pull/41/head
Rossen Georgiev 9 years ago
parent
commit
4f7e23d5a4
  1. 15
      docs/api/steam.client.builtins.rst
  2. 4
      steam/client/builtins/__init__.py
  3. 170
      steam/client/builtins/leaderboards.py
  4. 296
      steam/client/builtins/misc.py
  5. 145
      steam/client/builtins/unified_messages.py

15
docs/api/steam.client.builtins.rst

@ -27,3 +27,18 @@ Misc
:undoc-members:
:show-inheritance:
Unified Messages
----------------
.. automodule:: steam.client.builtins.unified_messages
:members:
:undoc-members:
:show-inheritance:
Leaderboards
------------
.. automodule:: steam.client.builtins.leaderboards
:members:
:undoc-members:
:show-inheritance:

4
steam/client/builtins/__init__.py

@ -4,8 +4,10 @@ All high level features of :class:`steam.client.SteamClient` are implemented her
from steam.client.builtins.misc import Misc
from steam.client.builtins.user import User
from steam.client.builtins.web import Web
from steam.client.builtins.unified_messages import UnifiedMessages
from steam.client.builtins.leaderboards import Leaderboards
class BuiltinBase(Misc, User, Web):
class BuiltinBase(Misc, User, Web, UnifiedMessages, Leaderboards):
"""
This object is used as base to implement all high level functionality.
The features are separated into submodules.

170
steam/client/builtins/leaderboards.py

@ -0,0 +1,170 @@
"""
Reading the leaderboards with :class:`SteamLeaderboard` is as easy as iterating over a list.
"""
import logging
from steam.core.msg import MsgProto
from steam.enums import EResult, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType
from steam.enums.emsg import EMsg
from steam.util import _range, chunks
from steam.util.throttle import ConstantRateLimit
class Leaderboards(object):
def __init__(self, *args, **kwargs):
super(Leaderboards, self).__init__(*args, **kwargs)
def get_leaderboard(self, app_id, name):
""".. versionadded:: 0.8.2
Find a leaderboard
:param app_id: application id
:type app_id: :class:`int`
:param name: leaderboard name
:type name: :class:`str`
:return: leaderboard instance
:rtype: :class:`SteamLeaderboard`
:raises: :class:`LookupError` on message timeout or error
"""
message = MsgProto(EMsg.ClientLBSFindOrCreateLB)
message.header.routing_appid = app_id
message.body.app_id = app_id
message.body.leaderboard_name = name
message.body.create_if_not_found = False
resp = self.send_job_and_wait(message, timeout=15)
if not resp:
raise LookupError("Didn't receive response within 15seconds :(")
if resp.eresult != EResult.OK:
raise LookupError(EResult(resp.eresult))
return SteamLeaderboard(self, app_id, name, resp)
class SteamLeaderboard(object):
""".. versionadded:: 0.8.2
Steam leaderboard object.
Generated via :meth:`Misc.get_leaderboard()`
Works more or less like a :class:`list` to access entries.
.. note::
Each slice will produce a message to steam.
Steam and protobufs might not like large slices.
Avoid accessing individual entries by index and instead use iteration or well sized slices.
Example usage:
.. code:: python
lb = client.get_leaderboard(...)
for entry in lb[:100]: # top 100
print entry
"""
app_id = 0
name = '' #: leaderboard name
id = 0 #: leaderboard id
entry_count = 0
sort_method = ELeaderboardSortMethod.NONE #: :class:`steam.enums.common.ELeaderboardSortMethod`
display_type = ELeaderboardDisplayType.NONE #: :class:`steam.enums.common.ELeaderboardDisplayType`
data_request = ELeaderboardDataRequest.Global #: :class:`steam.enums.common.ELeaderboardDataRequest`
def __init__(self, steam, app_id, name, data):
self._steam = steam
self.app_id = app_id
for field in data.DESCRIPTOR.fields:
if field.name.startswith('leaderboard_'):
self.__dict__[field.name.replace('leaderboard_', '')] = getattr(data, field.name)
self.sort_method = ELeaderboardSortMethod(self.sort_method)
self.display_type = ELeaderboardDisplayType(self.display_type)
def __repr__(self):
return "<%s(%d, %s, %d, %s, %s)>" % (
self.__class__.__name__,
self.app_id,
repr(self.name),
len(self),
self.sort_method,
self.display_type,
)
def __len__(self):
return self.entry_count
def get_entries(self, start=0, end=0, data_request=ELeaderboardDataRequest.Global):
"""Get leaderboard entries.
:param start: start entry, not index (e.g. rank 1 is `start=1`)
:type start: :class:`int`
:param end: end entry, not index (e.g. only one entry then `start=1,end=1`)
:type end: :class:`int`
:param data_request: data being requested
:type data_request: :class:`steam.enums.common.ELeaderboardDataRequest`
:return: a list of entries, see `CMsgClientLBSGetLBEntriesResponse`
:rtype: :class:`list`
:raises: :class:`LookupError` on message timeout or error
"""
message = MsgProto(EMsg.ClientLBSGetLBEntries)
message.body.app_id = self.app_id
message.body.leaderboard_id = self.id
message.body.range_start = start
message.body.range_end = end
message.body.leaderboard_data_request = data_request
resp = self._steam.send_job_and_wait(message, timeout=15)
if not resp:
raise LookupError("Didn't receive response within 15seconds :(")
if resp.eresult != EResult.OK:
raise LookupError(EResult(resp.eresult))
return resp.entries
def __getitem__(self, x):
if isinstance(x, slice):
stop_max = len(self)
start = 0 if x.start is None else x.start if x.start >= 0 else max(0, x.start + stop_max)
stop = stop_max if x.stop is None else x.stop if x.stop >= 0 else max(0, x.stop + stop_max)
step = x.step or 1
if step < 0:
start, stop = stop, start
step = abs(step)
else:
start, stop, step = x, x + 1, 1
if start >= stop: return []
entries = self.get_entries(start+1, stop, self.data_request)
return [entries[i] for i in _range(0, len(entries), step)]
def get_iter(self, times, seconds, chunk_size=2000):
"""Make a iterator over the entries
See :class:`steam.util.throttle.ConstantRateLimit` for ``times`` and ``seconds`` parameters.
:param chunk_size: number of entries per request
:type chunk_size: :class:`int`
:returns: generator object
:rtype: :class:`generator`
The iterator essentially buffers ``chuck_size`` number of entries, and ensures
we are not sending messages too fast.
For example, the ``__iter__`` method on this class uses ``get_iter(1, 1, 2000)``
"""
def entry_generator():
with ConstantRateLimit(times, seconds, use_gevent=True) as r:
for entries in chunks(self, chunk_size):
if not entries:
raise StopIteration
for entry in entries:
yield entry
r.wait()
return entry_generator()
def __iter__(self):
return self.get_iter(1, 1, 2000)

296
steam/client/builtins/misc.py

@ -1,22 +1,14 @@
"""
Various features that don't have a category
"""
import logging
from eventemitter import EventEmitter
from steam.core.msg import MsgProto, get_um
from steam.enums import EResult, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType
from steam.core.msg import MsgProto
from steam.enums.emsg import EMsg
from steam.util import WeakRefKeyDict, _range, chunks, proto_fill_from_dict
from steam.util.throttle import ConstantRateLimit
class Misc(object):
def __init__(self, *args, **kwargs):
super(Misc, self).__init__(*args, **kwargs)
name = "%s.unified_messages" % self.__class__.__name__
self.unified_messages = SteamUnifiedMessages(self, name) #: instance of :class:`SteamUnifiedMessages`
def games_played(self, app_ids):
"""
Set the application being played by the user
@ -35,289 +27,3 @@ class Misc(object):
message.body.games_played.extend(map(lambda x: GamePlayed(game_id=x), app_ids))
self.send(message)
def get_leaderboard(self, app_id, name):
""".. versionadded:: 0.8.2
Find a leaderboard
:param app_id: application id
:type app_id: :class:`int`
:param name: leaderboard name
:type name: :class:`str`
:return: leaderboard instance
:rtype: :class:`SteamLeaderboard`
:raises: :class:`LookupError` on message timeout or error
"""
message = MsgProto(EMsg.ClientLBSFindOrCreateLB)
message.header.routing_appid = app_id
message.body.app_id = app_id
message.body.leaderboard_name = name
message.body.create_if_not_found = False
resp = self.send_job_and_wait(message, timeout=15)
if not resp:
raise LookupError("Didn't receive response within 15seconds :(")
if resp.eresult != EResult.OK:
raise LookupError(EResult(resp.eresult))
return SteamLeaderboard(self, app_id, name, resp)
class SteamUnifiedMessages(EventEmitter):
"""Simple API for send/recv of unified messages
Incoming messages are emitted as events once with their ``jobid``
and once with their method name (e.g. ``Player.GetGameBadgeLevels#1``)
Example code:
.. code:: python
response = client.unified_messages.send_and_wait('Player.GetGameBadgeLevels#1', {
'property': 1,
'something': 'value',
})
# or alternatively
jobid = client.unified_message.send('Player.GetGameBadgeLevels#1', {'something': 1})
response, = client.unified_message.wait_event(jobid)
# or
message = client.unified_message.get('Player.GetGameBadgeLevels#1')
message.something = 1
response = client.unified_message.send_and_wait(message)
"""
def __init__(self, steam, logger_name=None):
self._LOG = logging.getLogger(logger_name if logger_name else self.__class__.__name__)
self._steam = steam
self._data = WeakRefKeyDict()
steam.on(EMsg.ServiceMethod, self._handle_service_method)
steam.on(EMsg.ClientServiceMethodResponse, self._handle_client_service_method)
def emit(self, event, *args):
if event is not None:
self._LOG.debug("Emit event: %s" % repr(event))
EventEmitter.emit(self, event, *args)
def _handle_service_method(self, message):
self.emit(message.header.target_job_name, message.body)
def _handle_client_service_method(self, message):
method_name = message.body.method_name
proto = get_um(method_name, response=True)
if proto is None:
self._LOG("Unable to find proto for %s" % repr(method_name))
return
resp = proto()
resp.ParseFromString(message.body.serialized_method_response)
self.emit(method_name, resp)
jobid = message.header.jobid_target
if jobid not in (-1, 18446744073709551615):
self.emit("job_%d" % jobid, resp)
def get(self, method_name):
"""Get request proto instance for given methed name
:param method_name: name for the method (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str`
:return: proto message instance, or :class:`None` if not found
"""
proto = get_um(method_name)
if proto is None:
return None
message = proto()
self._data[message] = method_name
return message
def send(self, message, params=None):
"""Send service method request
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters
:type params: :class:`dict`
:return: ``jobid`` event identifier
:rtype: :class:`str`
Listen for ``jobid`` on this object to catch the response.
.. note::
If you listen for ``jobid`` on the client instance you will get the encapsulated message
"""
if isinstance(message, str):
message = self.get(message)
if message not in self._data:
raise ValueError("Supplied message seems to be invalid. Did you use 'get' method?")
if params:
proto_fill_from_dict(message, params)
capsule = MsgProto(EMsg.ClientServiceMethod)
capsule.body.method_name = self._data[message]
capsule.body.serialized_method = message.SerializeToString()
return self._steam.send_job(capsule)
def send_and_wait(self, message, params=None, timeout=None, raises=False):
"""Send service method request and wait for response
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters
:type params: :class:`dict`
:param timeout: (optional) seconds to wait
:type timeout: :class:`int`
:param raises: (optional) On timeout if :class:`False` return :class:`None`, else raise :class:`gevent.Timeout`
:type raises: :class:`bool`
:return: response proto message instance
:rtype: proto message, :class:`None`
:raises: ``gevent.Timeout``
"""
job_id = self.send(message, params)
resp = self.wait_event(job_id, timeout, raises=raises)
if resp is None and not raises:
return None
else:
return resp[0]
class SteamLeaderboard(object):
""".. versionadded:: 0.8.2
Steam leaderboard object.
Generated via :meth:`Misc.get_leaderboard()`
Works more or less like a :class:`list` to access entries.
.. note::
Each slice will produce a message to steam.
Steam and protobufs might not like large slices.
Avoid accessing individual entries by index and instead use iteration or well sized slices.
Example usage:
.. code:: python
lb = client.get_leaderboard(...)
print len(lb)
for entry in lb[:100]: # top 100
pass
"""
app_id = 0
name = '' #: leaderboard name
id = 0 #: leaderboard id
entry_count = 0
sort_method = ELeaderboardSortMethod.NONE #: :class:`steam.enums.common.ELeaderboardSortMethod`
display_type = ELeaderboardDisplayType.NONE #: :class:`steam.enums.common.ELeaderboardDisplayType`
data_request = ELeaderboardDataRequest.Global #: :class:`steam.enums.common.ELeaderboardDataRequest`
def __init__(self, steam, app_id, name, data):
self._steam = steam
self.app_id = app_id
for field in data.DESCRIPTOR.fields:
if field.name.startswith('leaderboard_'):
self.__dict__[field.name.replace('leaderboard_', '')] = getattr(data, field.name)
self.sort_method = ELeaderboardSortMethod(self.sort_method)
self.display_type = ELeaderboardDisplayType(self.display_type)
def __repr__(self):
return "<%s(%d, %s, %d, %s, %s)>" % (
self.__class__.__name__,
self.app_id,
repr(self.name),
len(self),
self.sort_method,
self.display_type,
)
def __len__(self):
return self.entry_count
def get_entries(self, start=0, end=0, data_request=ELeaderboardDataRequest.Global):
"""Get leaderboard entries.
:param start: start entry, not index (e.g. rank 1 is `start=1`)
:type start: :class:`int`
:param end: end entry, not index (e.g. only one entry then `start=1,end=1`)
:type end: :class:`int`
:param data_request: data being requested
:type data_request: :class:`steam.enums.common.ELeaderboardDataRequest`
:return: a list of entries, see `CMsgClientLBSGetLBEntriesResponse`
:rtype: :class:`list`
:raises: :class:`LookupError` on message timeout or error
"""
message = MsgProto(EMsg.ClientLBSGetLBEntries)
message.body.app_id = self.app_id
message.body.leaderboard_id = self.id
message.body.range_start = start
message.body.range_end = end
message.body.leaderboard_data_request = data_request
resp = self._steam.send_job_and_wait(message, timeout=15)
if not resp:
raise LookupError("Didn't receive response within 15seconds :(")
if resp.eresult != EResult.OK:
raise LookupError(EResult(resp.eresult))
return resp.entries
def __getitem__(self, x):
if isinstance(x, slice):
stop_max = len(self)
start = 0 if x.start is None else x.start if x.start >= 0 else max(0, x.start + stop_max)
stop = stop_max if x.stop is None else x.stop if x.stop >= 0 else max(0, x.stop + stop_max)
step = x.step or 1
if step < 0:
start, stop = stop, start
step = abs(step)
else:
start, stop, step = x, x + 1, 1
if start >= stop: return []
entries = self.get_entries(start+1, stop, self.data_request)
return [entries[i] for i in _range(0, len(entries), step)]
def get_iter(self, times, seconds, chunk_size=2000):
"""Make a iterator over the entries
See :class:`steam.util.throttle.ConstantRateLimit` for ``times`` and ``seconds`` parameters.
:param chunk_size: number of entries per request
:type chunk_size: :class:`int`
:returns: generator object
:rtype: :class:`generator`
The iterator essentially buffers ``chuck_size`` number of entries, and ensures
we are not sending messages too fast.
For example, the ``__iter__`` method on this class uses ``get_iter(1, 1, 2000)``
"""
def entry_generator():
with ConstantRateLimit(times, seconds, use_gevent=True) as r:
for entries in chunks(self, chunk_size):
if not entries:
raise StopIteration
for entry in entries:
yield entry
r.wait()
return entry_generator()
def __iter__(self):
return self.get_iter(1, 1, 2000)

145
steam/client/builtins/unified_messages.py

@ -0,0 +1,145 @@
"""
:class:`SteamUnifiedMessages` provides a simply API to send and receive unified messages.
Example code:
.. code:: python
# the easy way
response = client.unified_messages.send_and_wait('Player.GetGameBadgeLevels#1', {
'property': 1,
'something': 'value',
})
# the other way
jobid = client.unified_message.send('Player.GetGameBadgeLevels#1', {'something': 1})
response, = client.unified_message.wait_event(jobid)
# i know what im doing, alright?
message = client.unified_message.get('Player.GetGameBadgeLevels#1')
message.something = 1
response = client.unified_message.send_and_wait(message)
"""
import logging
from eventemitter import EventEmitter
from steam.core.msg import MsgProto, get_um
from steam.enums import EResult
from steam.enums.emsg import EMsg
from steam.util import WeakRefKeyDict, proto_fill_from_dict
class UnifiedMessages(object):
def __init__(self, *args, **kwargs):
super(UnifiedMessages, self).__init__(*args, **kwargs)
name = "%s.unified_messages" % self.__class__.__name__
self.unified_messages = SteamUnifiedMessages(self, name) #: instance of :class:`SteamUnifiedMessages`
class SteamUnifiedMessages(EventEmitter):
"""Simple API for send/recv of unified messages
Incoming messages are emitted as events once with their ``jobid``
and once with their method name (e.g. ``Player.GetGameBadgeLevels#1``)
"""
def __init__(self, steam, logger_name=None):
self._LOG = logging.getLogger(logger_name if logger_name else self.__class__.__name__)
self._steam = steam
self._data = WeakRefKeyDict()
steam.on(EMsg.ServiceMethod, self._handle_service_method)
steam.on(EMsg.ClientServiceMethodResponse, self._handle_client_service_method)
def emit(self, event, *args):
if event is not None:
self._LOG.debug("Emit event: %s" % repr(event))
EventEmitter.emit(self, event, *args)
def _handle_service_method(self, message):
self.emit(message.header.target_job_name, message.body)
def _handle_client_service_method(self, message):
method_name = message.body.method_name
proto = get_um(method_name, response=True)
if proto is None:
self._LOG("Unable to find proto for %s" % repr(method_name))
return
resp = proto()
resp.ParseFromString(message.body.serialized_method_response)
self.emit(method_name, resp)
jobid = message.header.jobid_target
if jobid not in (-1, 18446744073709551615):
self.emit("job_%d" % jobid, resp)
def get(self, method_name):
"""Get request proto instance for given methed name
:param method_name: name for the method (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str`
:return: proto message instance, or :class:`None` if not found
"""
proto = get_um(method_name)
if proto is None:
return None
message = proto()
self._data[message] = method_name
return message
def send(self, message, params=None):
"""Send service method request
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters
:type params: :class:`dict`
:return: ``jobid`` event identifier
:rtype: :class:`str`
Listen for ``jobid`` on this object to catch the response.
.. note::
If you listen for ``jobid`` on the client instance you will get the encapsulated message
"""
if isinstance(message, str):
message = self.get(message)
if message not in self._data:
raise ValueError("Supplied message seems to be invalid. Did you use 'get' method?")
if params:
proto_fill_from_dict(message, params)
capsule = MsgProto(EMsg.ClientServiceMethod)
capsule.body.method_name = self._data[message]
capsule.body.serialized_method = message.SerializeToString()
return self._steam.send_job(capsule)
def send_and_wait(self, message, params=None, timeout=None, raises=False):
"""Send service method request and wait for response
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters
:type params: :class:`dict`
:param timeout: (optional) seconds to wait
:type timeout: :class:`int`
:param raises: (optional) On timeout if :class:`False` return :class:`None`, else raise :class:`gevent.Timeout`
:type raises: :class:`bool`
:return: response proto message instance
:rtype: proto message, :class:`None`
:raises: :class:`gevent.Timeout`
"""
job_id = self.send(message, params)
resp = self.wait_event(job_id, timeout, raises=raises)
if resp is None and not raises:
return None
else:
return resp[0]
Loading…
Cancel
Save