Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructure source module #56

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 5 additions & 127 deletions valve/source/__init__.py
Original file line number Diff line number Diff line change
@@ -1,128 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Oliver Ainsworth
from __future__ import absolute_import

from __future__ import (absolute_import,
unicode_literals, print_function, division)

import functools
import select
import socket
import warnings

import six


class NoResponseError(Exception):
"""Raised when a server querier doesn't receive a response."""


class QuerierClosedError(Exception):
"""Raised when attempting to use a querier after it's closed."""


class BaseQuerier(object):
"""Base class for implementing source server queriers.

When an instance of this class is initialised a socket is created.
It's important that, once a querier is to be discarded, the associated
socket be closed via :meth:`close`. For example:

.. code-block:: python

querier = valve.source.BaseQuerier(('...', 27015))
try:
querier.request(...)
finally:
querier.close()

When server queriers are used as context managers, the socket will
be cleaned up automatically. Hence it's preferably to use the `with`
statement over the `try`-`finally` pattern described above:

.. code-block:: python

with valve.source.BaseQuerier(('...', 27015)) as querier:
querier.request(...)

Once a querier has been closed, any attempts to make additional requests
will result in a :exc:`QuerierClosedError` to be raised.

:ivar host: Host requests will be sent to.
:ivar port: Port number requests will be sent to.
:ivar timeout: How long to wait for a response to a request.
"""

def __init__(self, address, timeout=5.0):
self.host = address[0]
self.port = address[1]
self.timeout = timeout
self._contextual = False
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

def __enter__(self):
self._contextual = True
return self

def __exit__(self, type_, exception, traceback):
self._contextual = False
self.close()

def _check_open(function):
# Wrap methods to raise QuerierClosedError when called
# after the querier has been closed.

@functools.wraps(function)
def wrapper(self, *args, **kwargs):
if self._socket is None:
raise QuerierClosedError
return function(self, *args, **kwargs)

return wrapper

def close(self):
"""Close the querier's socket.

It is safe to call this multiple times.
"""
if self._contextual:
warnings.warn("{0.__class__.__name__} used as context "
"manager but close called before exit".format(self))
if self._socket is not None:
self._socket.close()
self._socket = None

@_check_open
def request(self, *request):
"""Issue a request.

The given request segments will be encoded and combined to
form the final message that is sent to the configured address.

:param request: Request message segments.
:type request: valve.source.messages.Message

:raises QuerierClosedError: If the querier has been closed.
"""
request_final = b"".join(segment.encode() for segment in request)
self._socket.sendto(request_final, (self.host, self.port))

@_check_open
def get_response(self):
"""Wait for a response to a request.

:raises NoResponseError: If the configured :attr:`timeout` is
reached before a response is received.
:raises QuerierClosedError: If the querier has been closed.

:returns: The raw response as a :class:`bytes`.
"""
ready = select.select([self._socket], [], [], self.timeout)
if not ready[0]:
raise NoResponseError("Timed out waiting for response")
try:
data = ready[0][0].recv(1400)
except socket.error as exc:
six.raise_from(NoResponseError(exc))
return data

del _check_open
from .basequerier import BaseQuerier, NoResponseError, QuerierClosedError
from .a2s import ServerQuerier
from .master_server import MasterServerQuerier, Duplicates
from .util import Platform, ServerType
9 changes: 3 additions & 6 deletions valve/source/a2s.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@

import monotonic

import valve.source
from .basequerier import BaseQuerier, NoResponseError
from . import messages


# NOTE: backwards compatability; remove soon(tm)
NoResponseError = valve.source.NoResponseError


class ServerQuerier(valve.source.BaseQuerier):
class ServerQuerier(BaseQuerier):
"""Implements the A2S Source server query protocol.

https://developer.valvesoftware.com/wiki/Server_queries
Expand All @@ -30,7 +27,7 @@ def request(self, request):

def get_response(self):

data = valve.source.BaseQuerier.get_response(self)
data = BaseQuerier.get_response(self)

# According to https://developer.valvesoftware.com/wiki/Server_queries
# "TF2 currently does not split replies, expect A2S_PLAYER and
Expand Down
128 changes: 128 additions & 0 deletions valve/source/basequerier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Oliver Ainsworth

from __future__ import (absolute_import,
unicode_literals, print_function, division)

import functools
import select
import socket
import warnings

import six


class NoResponseError(Exception):
"""Raised when a server querier doesn't receive a response."""


class QuerierClosedError(Exception):
"""Raised when attempting to use a querier after it's closed."""


class BaseQuerier(object):
"""Base class for implementing source server queriers.

When an instance of this class is initialised a socket is created.
It's important that, once a querier is to be discarded, the associated
socket be closed via :meth:`close`. For example:

.. code-block:: python

querier = valve.source.BaseQuerier(('...', 27015))
try:
querier.request(...)
finally:
querier.close()

When server queriers are used as context managers, the socket will
be cleaned up automatically. Hence it's preferably to use the `with`
statement over the `try`-`finally` pattern described above:

.. code-block:: python

with valve.source.BaseQuerier(('...', 27015)) as querier:
querier.request(...)

Once a querier has been closed, any attempts to make additional requests
will result in a :exc:`QuerierClosedError` to be raised.

:ivar host: Host requests will be sent to.
:ivar port: Port number requests will be sent to.
:ivar timeout: How long to wait for a response to a request.
"""

def __init__(self, address, timeout=5.0):
self.host = address[0]
self.port = address[1]
self.timeout = timeout
self._contextual = False
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

def __enter__(self):
self._contextual = True
return self

def __exit__(self, type_, exception, traceback):
self._contextual = False
self.close()

def _check_open(function):
# Wrap methods to raise QuerierClosedError when called
# after the querier has been closed.

@functools.wraps(function)
def wrapper(self, *args, **kwargs):
if self._socket is None:
raise QuerierClosedError
return function(self, *args, **kwargs)

return wrapper

def close(self):
"""Close the querier's socket.

It is safe to call this multiple times.
"""
if self._contextual:
warnings.warn("{0.__class__.__name__} used as context "
"manager but close called before exit".format(self))
if self._socket is not None:
self._socket.close()
self._socket = None

@_check_open
def request(self, *request):
"""Issue a request.

The given request segments will be encoded and combined to
form the final message that is sent to the configured address.

:param request: Request message segments.
:type request: valve.source.messages.Message

:raises QuerierClosedError: If the querier has been closed.
"""
request_final = b"".join(segment.encode() for segment in request)
self._socket.sendto(request_final, (self.host, self.port))

@_check_open
def get_response(self):
"""Wait for a response to a request.

:raises NoResponseError: If the configured :attr:`timeout` is
reached before a response is received.
:raises QuerierClosedError: If the querier has been closed.

:returns: The raw response as a :class:`bytes`.
"""
ready = select.select([self._socket], [], [], self.timeout)
if not ready[0]:
raise NoResponseError("Timed out waiting for response")
try:
data = ready[0][0].recv(1400)
except socket.error as exc:
six.raise_from(NoResponseError(exc))
return data

del _check_open
6 changes: 3 additions & 3 deletions valve/source/master_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import six

import valve.source
from .basequerier import BaseQuerier, NoResponseError
from . import messages
from . import util

Expand Down Expand Up @@ -44,7 +44,7 @@ class Duplicates(enum.Enum):
STOP = "stop"


class MasterServerQuerier(valve.source.BaseQuerier):
class MasterServerQuerier(BaseQuerier):
"""Implements the Source master server query protocol

https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
Expand Down Expand Up @@ -103,7 +103,7 @@ def _query(self, region, filter_string):
region=region, address=last_addr, filter=filter_string))
try:
raw_response = self.get_response()
except valve.source.NoResponseError:
except NoResponseError:
return
else:
response = messages.MasterServerResponse.decode(raw_response)
Expand Down