20
20
from email .mime .application import MIMEApplication
21
21
from email .mime .multipart import MIMEMultipart
22
22
from email .parser import Parser
23
+ import httplib2
23
24
import io
24
25
import json
25
26
26
27
import six
27
28
28
29
from gcloud ._helpers import _LocalStack
30
+ from gcloud .exceptions import make_exception
29
31
from gcloud .storage import _implicit_environ
30
32
from gcloud .storage .connection import Connection
31
33
@@ -71,6 +73,54 @@ class NoContent(object):
71
73
status = 204
72
74
73
75
76
+ class _FutureDict (object ):
77
+ """Class to hold a future value for a deferred request.
78
+
79
+ Used by for requests that get sent in a :class:`Batch`.
80
+ """
81
+
82
+ @staticmethod
83
+ def get (key , default = None ):
84
+ """Stand-in for dict.get.
85
+
86
+ :type key: object
87
+ :param key: Hashable dictionary key.
88
+
89
+ :type default: object
90
+ :param default: Fallback value to dict.get.
91
+
92
+ :raises: :class:`KeyError` always since the future is intended to fail
93
+ as a dictionary.
94
+ """
95
+ raise KeyError ('Cannot get(%r, default=%r) on a future' % (
96
+ key , default ))
97
+
98
+ def __getitem__ (self , key ):
99
+ """Stand-in for dict[key].
100
+
101
+ :type key: object
102
+ :param key: Hashable dictionary key.
103
+
104
+ :raises: :class:`KeyError` always since the future is intended to fail
105
+ as a dictionary.
106
+ """
107
+ raise KeyError ('Cannot get item %r from a future' % (key ,))
108
+
109
+ def __setitem__ (self , key , value ):
110
+ """Stand-in for dict[key] = value.
111
+
112
+ :type key: object
113
+ :param key: Hashable dictionary key.
114
+
115
+ :type value: object
116
+ :param value: Dictionary value.
117
+
118
+ :raises: :class:`KeyError` always since the future is intended to fail
119
+ as a dictionary.
120
+ """
121
+ raise KeyError ('Cannot set %r -> %r on a future' % (key , value ))
122
+
123
+
74
124
class Batch (Connection ):
75
125
"""Proxy an underlying connection, batching up change operations.
76
126
@@ -86,9 +136,9 @@ def __init__(self, connection=None):
86
136
super (Batch , self ).__init__ ()
87
137
self ._connection = connection
88
138
self ._requests = []
89
- self ._responses = []
139
+ self ._target_objects = []
90
140
91
- def _do_request (self , method , url , headers , data ):
141
+ def _do_request (self , method , url , headers , data , target_object ):
92
142
"""Override Connection: defer actual HTTP request.
93
143
94
144
Only allow up to ``_MAX_BATCH_SIZE`` requests to be deferred.
@@ -109,22 +159,22 @@ def _do_request(self, method, url, headers, data):
109
159
and ``content`` (a string).
110
160
:returns: The HTTP response object and the content of the response.
111
161
"""
112
- if method == 'GET' :
113
- _req = self ._connection .http .request
114
- return _req (method = method , uri = url , headers = headers , body = data )
115
-
116
162
if len (self ._requests ) >= self ._MAX_BATCH_SIZE :
117
163
raise ValueError ("Too many deferred requests (max %d)" %
118
164
self ._MAX_BATCH_SIZE )
119
165
self ._requests .append ((method , url , headers , data ))
120
- return NoContent (), ''
121
-
122
- def finish (self ):
123
- """Submit a single `multipart/mixed` request w/ deferred requests.
124
-
125
- :rtype: list of tuples
126
- :returns: one ``(status, reason, payload)`` tuple per deferred request.
127
- :raises: ValueError if no requests have been deferred.
166
+ result = _FutureDict ()
167
+ self ._target_objects .append (target_object )
168
+ if target_object is not None :
169
+ target_object ._properties = result
170
+ return NoContent (), result
171
+
172
+ def _prepare_batch_request (self ):
173
+ """Prepares headers and body for a batch request.
174
+
175
+ :rtype: tuple (dict, string)
176
+ :returns: The pair of headers and body of the batch request to be sent.
177
+ :raises: :class:`ValueError` if no requests have been deferred.
128
178
"""
129
179
if len (self ._requests ) == 0 :
130
180
raise ValueError ("No deferred requests" )
@@ -146,14 +196,51 @@ def finish(self):
146
196
147
197
# Strip off redundant header text
148
198
_ , body = payload .split ('\n \n ' , 1 )
149
- headers = dict (multi ._headers )
199
+ return dict (multi ._headers ), body
200
+
201
+ def _finish_futures (self , responses ):
202
+ """Apply all the batch responses to the futures created.
203
+
204
+ :type responses: list of (headers, payload) tuples.
205
+ :param responses: List of headers and payloads from each response in
206
+ the batch.
207
+
208
+ :raises: :class:`ValueError` if no requests have been deferred.
209
+ """
210
+ # If a bad status occurs, we track it, but don't raise an exception
211
+ # until all futures have been populated.
212
+ exception_args = None
213
+
214
+ if len (self ._target_objects ) != len (responses ):
215
+ raise ValueError ('Expected a response for every request.' )
216
+
217
+ for target_object , sub_response in zip (self ._target_objects ,
218
+ responses ):
219
+ resp_headers , sub_payload = sub_response
220
+ if not 200 <= resp_headers .status < 300 :
221
+ exception_args = exception_args or (resp_headers ,
222
+ sub_payload )
223
+ elif target_object is not None :
224
+ target_object ._properties = sub_payload
225
+
226
+ if exception_args is not None :
227
+ raise make_exception (* exception_args )
228
+
229
+ def finish (self ):
230
+ """Submit a single `multipart/mixed` request w/ deferred requests.
231
+
232
+ :rtype: list of tuples
233
+ :returns: one ``(headers, payload)`` tuple per deferred request.
234
+ """
235
+ headers , body = self ._prepare_batch_request ()
150
236
151
237
url = '%s/batch' % self .API_BASE_URL
152
238
153
- _req = self ._connection ._make_request
154
- response , content = _req ('POST' , url , data = body , headers = headers )
155
- self ._responses = list (_unpack_batch_response (response , content ))
156
- return self ._responses
239
+ response , content = self ._connection ._make_request (
240
+ 'POST' , url , data = body , headers = headers )
241
+ responses = list (_unpack_batch_response (response , content ))
242
+ self ._finish_futures (responses )
243
+ return responses
157
244
158
245
@staticmethod
159
246
def current ():
@@ -199,7 +286,20 @@ def _generate_faux_mime_message(parser, response, content):
199
286
200
287
201
288
def _unpack_batch_response (response , content ):
202
- """Convert response, content -> [(status, reason, payload)]."""
289
+ """Convert response, content -> [(headers, payload)].
290
+
291
+ Creates a generator of tuples of emulating the responses to
292
+ :meth:`httplib2.Http.request` (a pair of headers and payload).
293
+
294
+ :type response: :class:`httplib2.Response`
295
+ :param response: HTTP response / headers from a request.
296
+
297
+ :type content: string
298
+ :param content: Response payload with a batch response.
299
+
300
+ :rtype: generator
301
+ :returns: A generator of header, payload pairs.
302
+ """
203
303
parser = Parser ()
204
304
message = _generate_faux_mime_message (parser , response , content )
205
305
@@ -208,10 +308,13 @@ def _unpack_batch_response(response, content):
208
308
209
309
for subrequest in message ._payload :
210
310
status_line , rest = subrequest ._payload .split ('\n ' , 1 )
211
- _ , status , reason = status_line .split (' ' , 2 )
212
- message = parser .parsestr (rest )
213
- payload = message ._payload
214
- ctype = message ['Content-Type' ]
311
+ _ , status , _ = status_line .split (' ' , 2 )
312
+ sub_message = parser .parsestr (rest )
313
+ payload = sub_message ._payload
314
+ ctype = sub_message ['Content-Type' ]
315
+ msg_headers = dict (sub_message ._headers )
316
+ msg_headers ['status' ] = status
317
+ headers = httplib2 .Response (msg_headers )
215
318
if ctype and ctype .startswith ('application/json' ):
216
319
payload = json .loads (payload )
217
- yield status , reason , payload
320
+ yield headers , payload
0 commit comments