Skip to content

Commit

Permalink
Merge pull request #483 from randomir/add-multi-region-support
Browse files Browse the repository at this point in the history
Add multi region support
  • Loading branch information
randomir committed Oct 18, 2021
2 parents d8b15e9 + 6019484 commit 4dee3ce
Show file tree
Hide file tree
Showing 29 changed files with 1,042 additions and 286 deletions.
1 change: 1 addition & 0 deletions docs/reference/resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Methods
:toctree: generated

Client.from_config
Client.get_regions
Client.get_solver
Client.get_solvers
Client.solvers
Expand Down
13 changes: 11 additions & 2 deletions dwave/cloud/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# NOTE: dwave.cloud.api module and submodules considered private for now!


import dwave.cloud.api.client
import dwave.cloud.api.constants
import dwave.cloud.api.exceptions
import dwave.cloud.api.models
import dwave.cloud.api.resources

from dwave.cloud.api.client import SAPIClient
from dwave.cloud.api.resources import Solvers, Problems
from dwave.cloud.api import client
from dwave.cloud.api import constants
from dwave.cloud.api import exceptions
from dwave.cloud.api import models
from dwave.cloud.api import resources

from dwave.cloud.api.client import DWaveAPIClient, SolverAPIClient, MetadataAPIClient
from dwave.cloud.api.resources import Solvers, Problems, Regions
175 changes: 122 additions & 53 deletions dwave/cloud/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import logging
from collections import deque, namedtuple

import requests
import urllib3
Expand All @@ -22,7 +23,7 @@
from dwave.cloud.utils import (
TimeoutingHTTPAdapter, BaseUrlSession, user_agent, is_caused_by)

__all__ = ['SAPIClient']
__all__ = ['DWaveAPIClient', 'SolverAPIClient', 'MetadataAPIClient']

logger = logging.getLogger(__name__)

Expand All @@ -47,39 +48,82 @@ def __get__(self, obj, objtype=None):
class LoggingSession(BaseUrlSession):
""":class:`.BaseUrlSession` extended to unify timeout exceptions and to log
all requests (and responses).
In addition to request logging, a history of responses, including exceptions
(up to `history_size`), is kept in :attr:`.history`.
"""

def request(self, method, *args, **kwargs):
def __init__(self, history_size: int = 0, **kwargs):
if not isinstance(history_size, int) or history_size < 0:
raise ValueError("non-negative integer value required for 'history_size'")

self.history = deque([], maxlen=history_size)

super().__init__(**kwargs)

def _request_unified(self, method: str, *args, **kwargs):
# timeout exceptions unified with regular request exceptions
try:
return super().request(method, *args, **kwargs)
except Exception as exc:
if is_caused_by(exc, (requests.exceptions.Timeout,
urllib3.exceptions.TimeoutError)):
raise exceptions.RequestTimeout(
request=getattr(exc, 'request', None),
response=getattr(exc, 'response', None)) from exc
else:
raise

RequestRecord = namedtuple('RequestRecord',
('request', 'response', 'exception'))

def request(self, method: str, *args, **kwargs):
callee = type(self).__name__
logger.trace("[%s] request(%r, *%r, **%r)",
callee, method, args, kwargs)

# unify timeout exceptions
try:
response = super().request(method, *args, **kwargs)
response = self._request_unified(method, *args, **kwargs)

rec = LoggingSession.RequestRecord(
request=response.request, response=response, exception=None)
self.history.append(rec)

except Exception as exc:
logger.trace("[%s] request failed with %r", callee, exc)
logger.debug("[%s] request failed with %r", callee, exc)

if is_caused_by(exc, (requests.exceptions.Timeout,
urllib3.exceptions.TimeoutError)):
raise exceptions.RequestTimeout from exc
else:
raise
req = getattr(exc, 'request', None)
if req:
logger.trace("[%s] failing request=%r", callee,
dict(method=req.method, url=req.url,
headers=req.headers, body=req.body))

res = getattr(exc, 'response', None)
if res:
logger.trace("[%s] failing response=%r", callee,
dict(status_code=res.status_code,
headers=res.headers, text=res.text))

rec = LoggingSession.RequestRecord(
request=req, response=res, exception=exc)
self.history.append(rec)

raise

logger.trace("[%s] request(...) = (code=%r, body=%r)",
callee, response.status_code, response.text)

return response


class SAPIClient:
"""Low-level SAPI client, as a thin wrapper around `requests.Session`,
that handles SAPI specifics like authentication and response parsing.
class DWaveAPIClient:
"""Low-level client for D-Wave APIs. A thin wrapper around
`requests.Session` that handles API specifics such as authentication,
response and error parsing, retrying, etc.
"""

DEFAULTS = {
'endpoint': constants.DEFAULT_API_ENDPOINT,
'endpoint': None,
'token': None,
'cert': None,

Expand All @@ -97,12 +141,15 @@ class SAPIClient:

# proxy urls, see :attr:`requests.Session.proxies`
'proxies': None,

# number of most recent request records to keep in :attr:`.session.history`
'history_size': 0,
}

# client instance config, populated on init from kwargs overridding DEFAULTS
config = None

# User-Agent string used in SAPI requests, as returned by
# User-Agent string used in API requests, as returned by
# :meth:`~dwave.cloud.utils.user_agent`, computed on first access and
# cached for the lifespan of the class.
# TODO: consider exposing "user_agent" config parameter
Expand All @@ -115,42 +162,8 @@ def __init__(self, **config):

self.session = self._create_session(self.config)

@classmethod
def from_client_config(cls, client):
"""Create SAPI client instance configured from a
:class:`~dwave.cloud.client.base.Client' instance.
"""

headers = client.headers.copy()
if client.connection_close:
headers.update({'Connection': 'close'})

opts = dict(
endpoint=client.endpoint,
token=client.token,
cert=client.client_cert,
timeout=client.request_timeout,
proxies=dict(
http=client.proxy,
https=client.proxy,
),
retry=dict(
total=client.http_retry_total,
connect=client.http_retry_connect,
read=client.http_retry_read,
redirect=client.http_retry_redirect,
status=client.http_retry_status,
raise_on_redirect=True,
raise_on_status=True,
respect_retry_after_header=True,
backoff_factor=client.http_retry_backoff_factor,
backoff_max=client.http_retry_backoff_max,
),
headers=client.headers,
verify=not client.permissive_ssl,
)

return cls(**opts)
def close(self):
self.session.close()

@staticmethod
def _retry_config(backoff_max=None, **kwargs):
Expand All @@ -172,11 +185,14 @@ def _create_session(cls, config):
# allow endpoint path to not end with /
# (handle incorrect user input when merging paths, see rfc3986, sec 5.2.3)
endpoint = config['endpoint']
if not endpoint:
raise ValueError("API endpoint undefined")
if not endpoint.endswith('/'):
endpoint += '/'

# configure request timeout and retries
session = LoggingSession(base_url=endpoint)
history_size = config['history_size']
session = LoggingSession(base_url=endpoint, history_size=history_size)
timeout = config['timeout']
retry = config['retry']
session.mount('http://',
Expand Down Expand Up @@ -206,7 +222,7 @@ def _create_session(cls, config):
session.hooks['response'].append(cls._raise_for_status)

# debug log
logger.debug("create_session from config={!r}".format(config))
logger.debug(f"{cls.__name__} session created using config={config!r}")

return session

Expand Down Expand Up @@ -262,3 +278,56 @@ def _raise_for_status(response, **kwargs):
raise exceptions.InternalServerError(**kw)
else:
raise exceptions.RequestError(**kw)


class SolverAPIClient(DWaveAPIClient):
"""Client for D-Wave's Solver API."""

def __init__(self, **config):
config.setdefault('endpoint', constants.DEFAULT_SOLVER_API_ENDPOINT)
super().__init__(**config)

@classmethod
def from_client_config(cls, client):
"""Create SAPI client instance configured from a
:class:`~dwave.cloud.client.base.Client' instance.
"""

headers = client.headers.copy()
if client.connection_close:
headers.update({'Connection': 'close'})

opts = dict(
endpoint=client.endpoint,
token=client.token,
cert=client.client_cert,
timeout=client.request_timeout,
proxies=dict(
http=client.proxy,
https=client.proxy,
),
retry=dict(
total=client.http_retry_total,
connect=client.http_retry_connect,
read=client.http_retry_read,
redirect=client.http_retry_redirect,
status=client.http_retry_status,
raise_on_redirect=True,
raise_on_status=True,
respect_retry_after_header=True,
backoff_factor=client.http_retry_backoff_factor,
backoff_max=client.http_retry_backoff_max,
),
headers=client.headers,
verify=not client.permissive_ssl,
)

return cls(**opts)


class MetadataAPIClient(DWaveAPIClient):
"""Client for D-Wave's Metadata API."""

def __init__(self, **config):
config.setdefault('endpoint', constants.DEFAULT_METADATA_API_ENDPOINT)
super().__init__(**config)
8 changes: 6 additions & 2 deletions dwave/cloud/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
import enum


# Default SAPI endpoint
DEFAULT_API_ENDPOINT = 'https://cloud.dwavesys.com/sapi/'
# Default API endpoints
DEFAULT_SOLVER_API_ENDPOINT = 'https://cloud.dwavesys.com/sapi/'

DEFAULT_METADATA_API_ENDPOINT = 'https://cloud.dwavesys.com/metadata/v1/'

DEFAULT_REGION = 'na-west-1'


class ProblemStatus(str, enum.Enum):
Expand Down
3 changes: 1 addition & 2 deletions dwave/cloud/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ class ResourceBadResponseError(RequestError):
"""Unexpected resource response"""

class InternalServerError(RequestError):
pass

"""internal server error occurred while request handling."""

class RequestTimeout(RequestError):
"""API request timed out"""
7 changes: 7 additions & 0 deletions dwave/cloud/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,10 @@ class ProblemSubmitError(BatchItemError):

class ProblemCancelError(BatchItemError):
pass


# region info on metadata api
class Region(BaseModel):
code: str
name: str
endpoint: str

0 comments on commit 4dee3ce

Please sign in to comment.