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

feat: add support for async http library aiohttp #1

Merged
merged 1 commit into from
Mar 8, 2022
Merged
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
72 changes: 57 additions & 15 deletions pyodata/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,31 @@
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):
# download metadata
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)
Expand All @@ -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"""
Expand All @@ -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
32 changes: 32 additions & 0 deletions pyodata/v2/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Utility class to standardize response

Author: Alberto Moio <email Alberto>, Nunzio Mauro <mnunzio90@gmail.com>
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)
74 changes: 64 additions & 10 deletions pyodata/v2/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from pyodata.exceptions import HttpError, PyODataException, ExpressionError, ProgramError
from . import model
from .response import Response

LOGGER_NAME = 'pyodata.service'

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -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"""

Expand Down