Skip to content

Commit e5b6ec1

Browse files
authored
Merge pull request #2 from mnunzio/async_python_no_duplicate_code
Async python no duplicate code
2 parents 65db1af + 00ad1dc commit e5b6ec1

File tree

3 files changed

+153
-25
lines changed

3 files changed

+153
-25
lines changed

pyodata/client.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,31 @@
66
import pyodata.v2.model
77
import pyodata.v2.service
88
from pyodata.exceptions import PyODataException, HttpError
9+
from pyodata.v2.response import Response
10+
11+
12+
async def _async_fetch_metadata(connection, url, logger):
13+
logger.info('Fetching metadata')
14+
15+
async with connection.get(url + '$metadata') as async_response:
16+
resp = Response()
17+
resp.url = async_response.url
18+
resp.headers = async_response.headers
19+
resp.status_code = async_response.status
20+
resp.content = await async_response.read()
21+
22+
return _common_fetch_metadata(resp, logger)
923

1024

1125
def _fetch_metadata(connection, url, logger):
1226
# download metadata
1327
logger.info('Fetching metadata')
1428
resp = connection.get(url + '$metadata')
1529

30+
return _common_fetch_metadata(resp, logger)
31+
32+
33+
def _common_fetch_metadata(resp, logger):
1634
logger.debug('Retrieved the response:\n%s\n%s',
1735
'\n'.join((f'H: {key}: {value}' for key, value in resp.headers.items())),
1836
resp.content)
@@ -37,6 +55,25 @@ class Client:
3755

3856
ODATA_VERSION_2 = 2
3957

58+
@staticmethod
59+
async def build_async_client(url, connection, odata_version=ODATA_VERSION_2, namespaces=None,
60+
config: pyodata.v2.model.Config = None, metadata: str = None):
61+
"""Create instance of the OData Client for given URL"""
62+
63+
logger = logging.getLogger('pyodata.client')
64+
65+
if odata_version == Client.ODATA_VERSION_2:
66+
67+
# sanitize url
68+
url = url.rstrip('/') + '/'
69+
70+
if metadata is None:
71+
metadata = await _async_fetch_metadata(connection, url, logger)
72+
else:
73+
logger.info('Using static metadata')
74+
return Client._build_service(logger, url, connection, odata_version, namespaces, config, metadata)
75+
raise PyODataException(f'No implementation for selected odata version {odata_version}')
76+
4077
def __new__(cls, url, connection, odata_version=ODATA_VERSION_2, namespaces=None,
4178
config: pyodata.v2.model.Config = None, metadata: str = None):
4279
"""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
5390
else:
5491
logger.info('Using static metadata')
5592

56-
if config is not None and namespaces is not None:
57-
raise PyODataException('You cannot pass namespaces and config at the same time')
93+
return Client._build_service(logger, url, connection, odata_version, namespaces, config, metadata)
94+
raise PyODataException(f'No implementation for selected odata version {odata_version}')
5895

59-
if config is None:
60-
config = pyodata.v2.model.Config()
96+
@staticmethod
97+
def _build_service(logger, url, connection, odata_version=ODATA_VERSION_2, namespaces=None,
98+
config: pyodata.v2.model.Config = None, metadata: str = None):
6199

62-
if namespaces is not None:
63-
warnings.warn("Passing namespaces directly is deprecated. Use class Config instead", DeprecationWarning)
64-
config.namespaces = namespaces
100+
if config is not None and namespaces is not None:
101+
raise PyODataException('You cannot pass namespaces and config at the same time')
65102

66-
# create model instance from received metadata
67-
logger.info('Creating OData Schema (version: %d)', odata_version)
68-
schema = pyodata.v2.model.MetadataBuilder(metadata, config=config).build()
103+
if config is None:
104+
config = pyodata.v2.model.Config()
69105

70-
# create service instance based on model we have
71-
logger.info('Creating OData Service (version: %d)', odata_version)
72-
service = pyodata.v2.service.Service(url, schema, connection, config=config)
106+
if namespaces is not None:
107+
warnings.warn("Passing namespaces directly is deprecated. Use class Config instead", DeprecationWarning)
108+
config.namespaces = namespaces
73109

74-
return service
110+
# create model instance from received metadata
111+
logger.info('Creating OData Schema (version: %d)', odata_version)
112+
schema = pyodata.v2.model.MetadataBuilder(metadata, config=config).build()
75113

76-
raise PyODataException(f'No implementation for selected odata version {odata_version}')
114+
# create service instance based on model we have
115+
logger.info('Creating OData Service (version: %d)', odata_version)
116+
service = pyodata.v2.service.Service(url, schema, connection, config=config)
117+
118+
return service

pyodata/v2/response.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Utility class to standardize response
3+
4+
Author: Alberto Moio <email Alberto>, Nunzio Mauro <[email protected]>
5+
Date: 2017-08-21
6+
"""
7+
import json
8+
9+
10+
class Response:
11+
"""Representation of http response in a standard form already used by handlers"""
12+
13+
__attrs__ = [
14+
'content', 'status_code', 'headers', 'url'
15+
]
16+
17+
def __init__(self):
18+
self.status_code = None
19+
self.headers = None
20+
self.url = None
21+
self.content = None
22+
23+
@property
24+
def text(self):
25+
"""Textual representation of response content"""
26+
27+
return self.content.decode('utf-8')
28+
29+
def json(self):
30+
"""JSON representation of response content"""
31+
32+
return json.loads(self.text)

pyodata/v2/service.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from pyodata.exceptions import HttpError, PyODataException, ExpressionError, ProgramError
2020
from . import model
21+
from .response import Response
2122

2223
LOGGER_NAME = 'pyodata.service'
2324

@@ -292,14 +293,7 @@ def add_headers(self, value):
292293

293294
self._headers.update(value)
294295

295-
def execute(self):
296-
"""Fetches HTTP response and returns processed result
297-
298-
Sends the query-request to the OData service, returning a client-side Enumerable for
299-
subsequent in-memory operations.
300-
301-
Fetches HTTP response and returns processed result"""
302-
296+
def _build_request(self):
303297
if self._next_url:
304298
url = self._next_url
305299
else:
@@ -315,10 +309,47 @@ def execute(self):
315309
if body:
316310
self._logger.debug(' body: %s', body)
317311

318-
params = urlencode(self.get_query_params())
312+
params = self.get_query_params()
313+
314+
return url, body, headers, params
315+
316+
async def async_execute(self):
317+
"""Fetches HTTP response and returns processed result
318+
319+
Sends the query-request to the OData service, returning a client-side Enumerable for
320+
subsequent in-memory operations.
321+
322+
Fetches HTTP response and returns processed result"""
323+
324+
url, body, headers, params = self._build_request()
325+
async with self._connection.request(self.get_method(),
326+
url,
327+
headers=headers,
328+
params=params,
329+
data=body) as async_response:
330+
response = Response()
331+
response.url = async_response.url
332+
response.headers = async_response.headers
333+
response.status_code = async_response.status
334+
response.content = await async_response.read()
335+
return self._call_handler(response)
336+
337+
def execute(self):
338+
"""Fetches HTTP response and returns processed result
339+
340+
Sends the query-request to the OData service, returning a client-side Enumerable for
341+
subsequent in-memory operations.
342+
343+
Fetches HTTP response and returns processed result"""
344+
345+
url, body, headers, params = self._build_request()
346+
319347
response = self._connection.request(
320-
self.get_method(), url, headers=headers, params=params, data=body)
348+
self.get_method(), url, headers=headers, params=urlencode(params), data=body)
321349

350+
return self._call_handler(response)
351+
352+
def _call_handler(self, response):
322353
self._logger.debug('Received response')
323354
self._logger.debug(' url: %s', response.url)
324355
self._logger.debug(' headers: %s', response.headers)
@@ -858,6 +889,19 @@ def __getattr__(self, attr):
858889
raise AttributeError('EntityType {0} does not have Property {1}: {2}'
859890
.format(self._entity_type.name, attr, str(ex)))
860891

892+
async def getattr(self, attr):
893+
"""Get cached value of attribute or do async call to service to recover attribute value"""
894+
try:
895+
return self._cache[attr]
896+
except KeyError:
897+
try:
898+
value = await self.get_proprty(attr).async_execute()
899+
self._cache[attr] = value
900+
return value
901+
except KeyError as ex:
902+
raise AttributeError('EntityType {0} does not have Property {1}: {2}'
903+
.format(self._entity_type.name, attr, str(ex)))
904+
861905
def nav(self, nav_property):
862906
"""Navigates to given navigation property and returns the EntitySetProxy"""
863907

@@ -1686,6 +1730,16 @@ def http_get(self, path, connection=None):
16861730

16871731
return conn.get(urljoin(self._url, path))
16881732

1733+
async def async_http_get(self, path, connection=None):
1734+
"""HTTP GET response for the passed path in the service"""
1735+
1736+
conn = connection
1737+
if conn is None:
1738+
conn = self._connection
1739+
1740+
async with conn.get(urljoin(self._url, path)) as resp:
1741+
return resp
1742+
16891743
def http_get_odata(self, path, handler, connection=None):
16901744
"""HTTP GET request proxy for the passed path in the service"""
16911745

0 commit comments

Comments
 (0)