diff --git a/pyodata/client.py b/pyodata/client.py index d7fb9377..d958465e 100644 --- a/pyodata/client.py +++ b/pyodata/client.py @@ -6,6 +6,20 @@ import pyodata.v2.model import pyodata.v2.service from pyodata.exceptions import PyODataException, HttpError +from pyodata.v2.response import Response + + +async def _async_fetch_metadata(connection, url, logger): + logger.info('Fetching metadata') + + async with connection.get(url + '$metadata') as async_response: + resp = Response() + resp.url = async_response.url + resp.headers = async_response.headers + resp.status_code = async_response.status + resp.content = await async_response.read() + + return _common_fetch_metadata(resp, logger) def _fetch_metadata(connection, url, logger): @@ -13,6 +27,10 @@ def _fetch_metadata(connection, url, logger): logger.info('Fetching metadata') resp = connection.get(url + '$metadata') + return _common_fetch_metadata(resp, logger) + + +def _common_fetch_metadata(resp, logger): logger.debug('Retrieved the response:\n%s\n%s', '\n'.join((f'H: {key}: {value}' for key, value in resp.headers.items())), resp.content) @@ -37,6 +55,25 @@ class Client: ODATA_VERSION_2 = 2 + @staticmethod + async def build_async_client(url, connection, odata_version=ODATA_VERSION_2, namespaces=None, + config: pyodata.v2.model.Config = None, metadata: str = None): + """Create instance of the OData Client for given URL""" + + logger = logging.getLogger('pyodata.client') + + if odata_version == Client.ODATA_VERSION_2: + + # sanitize url + url = url.rstrip('/') + '/' + + if metadata is None: + metadata = await _async_fetch_metadata(connection, url, logger) + else: + logger.info('Using static metadata') + return Client._build_service(logger, url, connection, odata_version, namespaces, config, metadata) + raise PyODataException(f'No implementation for selected odata version {odata_version}') + def __new__(cls, url, connection, odata_version=ODATA_VERSION_2, namespaces=None, config: pyodata.v2.model.Config = None, metadata: str = None): """Create instance of the OData Client for given URL""" @@ -53,24 +90,29 @@ def __new__(cls, url, connection, odata_version=ODATA_VERSION_2, namespaces=None else: logger.info('Using static metadata') - if config is not None and namespaces is not None: - raise PyODataException('You cannot pass namespaces and config at the same time') + return Client._build_service(logger, url, connection, odata_version, namespaces, config, metadata) + raise PyODataException(f'No implementation for selected odata version {odata_version}') - if config is None: - config = pyodata.v2.model.Config() + @staticmethod + def _build_service(logger, url, connection, odata_version=ODATA_VERSION_2, namespaces=None, + config: pyodata.v2.model.Config = None, metadata: str = None): - if namespaces is not None: - warnings.warn("Passing namespaces directly is deprecated. Use class Config instead", DeprecationWarning) - config.namespaces = namespaces + if config is not None and namespaces is not None: + raise PyODataException('You cannot pass namespaces and config at the same time') - # create model instance from received metadata - logger.info('Creating OData Schema (version: %d)', odata_version) - schema = pyodata.v2.model.MetadataBuilder(metadata, config=config).build() + if config is None: + config = pyodata.v2.model.Config() - # create service instance based on model we have - logger.info('Creating OData Service (version: %d)', odata_version) - service = pyodata.v2.service.Service(url, schema, connection, config=config) + if namespaces is not None: + warnings.warn("Passing namespaces directly is deprecated. Use class Config instead", DeprecationWarning) + config.namespaces = namespaces - return service + # create model instance from received metadata + logger.info('Creating OData Schema (version: %d)', odata_version) + schema = pyodata.v2.model.MetadataBuilder(metadata, config=config).build() - raise PyODataException(f'No implementation for selected odata version {odata_version}') + # create service instance based on model we have + logger.info('Creating OData Service (version: %d)', odata_version) + service = pyodata.v2.service.Service(url, schema, connection, config=config) + + return service diff --git a/pyodata/v2/response.py b/pyodata/v2/response.py new file mode 100644 index 00000000..2dce7056 --- /dev/null +++ b/pyodata/v2/response.py @@ -0,0 +1,32 @@ +""" +Utility class to standardize response + +Author: Alberto Moio , Nunzio Mauro +Date: 2017-08-21 +""" +import json + + +class Response: + """Representation of http response in a standard form already used by handlers""" + + __attrs__ = [ + 'content', 'status_code', 'headers', 'url' + ] + + def __init__(self): + self.status_code = None + self.headers = None + self.url = None + self.content = None + + @property + def text(self): + """Textual representation of response content""" + + return self.content.decode('utf-8') + + def json(self): + """JSON representation of response content""" + + return json.loads(self.text) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index f75f0d92..05dda32d 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -18,6 +18,7 @@ from pyodata.exceptions import HttpError, PyODataException, ExpressionError, ProgramError from . import model +from .response import Response LOGGER_NAME = 'pyodata.service' @@ -292,14 +293,7 @@ def add_headers(self, value): self._headers.update(value) - def execute(self): - """Fetches HTTP response and returns processed result - - Sends the query-request to the OData service, returning a client-side Enumerable for - subsequent in-memory operations. - - Fetches HTTP response and returns processed result""" - + def _build_request(self): if self._next_url: url = self._next_url else: @@ -315,10 +309,47 @@ def execute(self): if body: self._logger.debug(' body: %s', body) - params = urlencode(self.get_query_params()) + params = self.get_query_params() + + return url, body, headers, params + + async def async_execute(self): + """Fetches HTTP response and returns processed result + + Sends the query-request to the OData service, returning a client-side Enumerable for + subsequent in-memory operations. + + Fetches HTTP response and returns processed result""" + + url, body, headers, params = self._build_request() + async with self._connection.request(self.get_method(), + url, + headers=headers, + params=params, + data=body) as async_response: + response = Response() + response.url = async_response.url + response.headers = async_response.headers + response.status_code = async_response.status + response.content = await async_response.read() + return self._call_handler(response) + + def execute(self): + """Fetches HTTP response and returns processed result + + Sends the query-request to the OData service, returning a client-side Enumerable for + subsequent in-memory operations. + + Fetches HTTP response and returns processed result""" + + url, body, headers, params = self._build_request() + response = self._connection.request( - self.get_method(), url, headers=headers, params=params, data=body) + self.get_method(), url, headers=headers, params=urlencode(params), data=body) + return self._call_handler(response) + + def _call_handler(self, response): self._logger.debug('Received response') self._logger.debug(' url: %s', response.url) self._logger.debug(' headers: %s', response.headers) @@ -858,6 +889,19 @@ def __getattr__(self, attr): raise AttributeError('EntityType {0} does not have Property {1}: {2}' .format(self._entity_type.name, attr, str(ex))) + async def getattr(self, attr): + """Get cached value of attribute or do async call to service to recover attribute value""" + try: + return self._cache[attr] + except KeyError: + try: + value = await self.get_proprty(attr).async_execute() + self._cache[attr] = value + return value + except KeyError as ex: + raise AttributeError('EntityType {0} does not have Property {1}: {2}' + .format(self._entity_type.name, attr, str(ex))) + def nav(self, nav_property): """Navigates to given navigation property and returns the EntitySetProxy""" @@ -1686,6 +1730,16 @@ def http_get(self, path, connection=None): return conn.get(urljoin(self._url, path)) + async def async_http_get(self, path, connection=None): + """HTTP GET response for the passed path in the service""" + + conn = connection + if conn is None: + conn = self._connection + + async with conn.get(urljoin(self._url, path)) as resp: + return resp + def http_get_odata(self, path, handler, connection=None): """HTTP GET request proxy for the passed path in the service"""