5 changed files with 334 additions and 296 deletions
@ -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) |
@ -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…
Reference in new issue