Skip to content

Commit

Permalink
Merge pull request #2 from mnunzio/async_python_no_duplicate_code
Browse files Browse the repository at this point in the history
Async python no duplicate code
  • Loading branch information
Albo90 authored Mar 8, 2022
2 parents 65db1af + 00ad1dc commit e5b6ec1
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 25 deletions.
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

0 comments on commit e5b6ec1

Please sign in to comment.