##// END OF EJS Templates
wireprototypes: make `baseprotocolhandler` methods abstract...
Matt Harbison -
r53020:fd200f5b default
parent child Browse files
Show More
@@ -1,460 +1,467
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 from __future__ import annotations
6 from __future__ import annotations
7
7
8 import abc
8 import abc
9 import typing
9 import typing
10
10
11 from typing import (
11 from typing import (
12 Protocol,
12 Protocol,
13 )
13 )
14
14
15 from .node import (
15 from .node import (
16 bin,
16 bin,
17 hex,
17 hex,
18 )
18 )
19 from .i18n import _
19 from .i18n import _
20 from .thirdparty import attr
20 from .thirdparty import attr
21
21
22 # Force pytype to use the non-vendored package
22 # Force pytype to use the non-vendored package
23 if typing.TYPE_CHECKING:
23 if typing.TYPE_CHECKING:
24 # noinspection PyPackageRequirements
24 # noinspection PyPackageRequirements
25 import attr
25 import attr
26
26
27 from . import (
27 from . import (
28 error,
28 error,
29 util,
29 util,
30 )
30 )
31 from .utils import compression
31 from .utils import compression
32
32
33 # Names of the SSH protocol implementations.
33 # Names of the SSH protocol implementations.
34 SSHV1 = b'ssh-v1'
34 SSHV1 = b'ssh-v1'
35
35
36 NARROWCAP = b'exp-narrow-1'
36 NARROWCAP = b'exp-narrow-1'
37 ELLIPSESCAP1 = b'exp-ellipses-1'
37 ELLIPSESCAP1 = b'exp-ellipses-1'
38 ELLIPSESCAP = b'exp-ellipses-2'
38 ELLIPSESCAP = b'exp-ellipses-2'
39 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
39 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
40
40
41 # All available wire protocol transports.
41 # All available wire protocol transports.
42 TRANSPORTS = {
42 TRANSPORTS = {
43 SSHV1: {
43 SSHV1: {
44 b'transport': b'ssh',
44 b'transport': b'ssh',
45 b'version': 1,
45 b'version': 1,
46 },
46 },
47 b'http-v1': {
47 b'http-v1': {
48 b'transport': b'http',
48 b'transport': b'http',
49 b'version': 1,
49 b'version': 1,
50 },
50 },
51 }
51 }
52
52
53
53
54 class bytesresponse:
54 class bytesresponse:
55 """A wire protocol response consisting of raw bytes."""
55 """A wire protocol response consisting of raw bytes."""
56
56
57 def __init__(self, data):
57 def __init__(self, data):
58 self.data = data
58 self.data = data
59
59
60
60
61 class ooberror:
61 class ooberror:
62 """wireproto reply: failure of a batch of operation
62 """wireproto reply: failure of a batch of operation
63
63
64 Something failed during a batch call. The error message is stored in
64 Something failed during a batch call. The error message is stored in
65 `self.message`.
65 `self.message`.
66 """
66 """
67
67
68 def __init__(self, message):
68 def __init__(self, message):
69 self.message = message
69 self.message = message
70
70
71
71
72 class pushres:
72 class pushres:
73 """wireproto reply: success with simple integer return
73 """wireproto reply: success with simple integer return
74
74
75 The call was successful and returned an integer contained in `self.res`.
75 The call was successful and returned an integer contained in `self.res`.
76 """
76 """
77
77
78 def __init__(self, res, output):
78 def __init__(self, res, output):
79 self.res = res
79 self.res = res
80 self.output = output
80 self.output = output
81
81
82
82
83 class pusherr:
83 class pusherr:
84 """wireproto reply: failure
84 """wireproto reply: failure
85
85
86 The call failed. The `self.res` attribute contains the error message.
86 The call failed. The `self.res` attribute contains the error message.
87 """
87 """
88
88
89 def __init__(self, res, output):
89 def __init__(self, res, output):
90 self.res = res
90 self.res = res
91 self.output = output
91 self.output = output
92
92
93
93
94 class streamres:
94 class streamres:
95 """wireproto reply: binary stream
95 """wireproto reply: binary stream
96
96
97 The call was successful and the result is a stream.
97 The call was successful and the result is a stream.
98
98
99 Accepts a generator containing chunks of data to be sent to the client.
99 Accepts a generator containing chunks of data to be sent to the client.
100
100
101 ``prefer_uncompressed`` indicates that the data is expected to be
101 ``prefer_uncompressed`` indicates that the data is expected to be
102 uncompressable and that the stream should therefore use the ``none``
102 uncompressable and that the stream should therefore use the ``none``
103 engine.
103 engine.
104 """
104 """
105
105
106 def __init__(self, gen=None, prefer_uncompressed=False):
106 def __init__(self, gen=None, prefer_uncompressed=False):
107 self.gen = gen
107 self.gen = gen
108 self.prefer_uncompressed = prefer_uncompressed
108 self.prefer_uncompressed = prefer_uncompressed
109
109
110
110
111 class streamreslegacy:
111 class streamreslegacy:
112 """wireproto reply: uncompressed binary stream
112 """wireproto reply: uncompressed binary stream
113
113
114 The call was successful and the result is a stream.
114 The call was successful and the result is a stream.
115
115
116 Accepts a generator containing chunks of data to be sent to the client.
116 Accepts a generator containing chunks of data to be sent to the client.
117
117
118 Like ``streamres``, but sends an uncompressed data for "version 1" clients
118 Like ``streamres``, but sends an uncompressed data for "version 1" clients
119 using the application/mercurial-0.1 media type.
119 using the application/mercurial-0.1 media type.
120 """
120 """
121
121
122 def __init__(self, gen=None):
122 def __init__(self, gen=None):
123 self.gen = gen
123 self.gen = gen
124
124
125
125
126 # list of nodes encoding / decoding
126 # list of nodes encoding / decoding
127 def decodelist(l, sep=b' '):
127 def decodelist(l, sep=b' '):
128 if l:
128 if l:
129 return [bin(v) for v in l.split(sep)]
129 return [bin(v) for v in l.split(sep)]
130 return []
130 return []
131
131
132
132
133 def encodelist(l, sep=b' '):
133 def encodelist(l, sep=b' '):
134 try:
134 try:
135 return sep.join(map(hex, l))
135 return sep.join(map(hex, l))
136 except TypeError:
136 except TypeError:
137 raise
137 raise
138
138
139
139
140 # batched call argument encoding
140 # batched call argument encoding
141
141
142
142
143 def escapebatcharg(plain):
143 def escapebatcharg(plain):
144 return (
144 return (
145 plain.replace(b':', b':c')
145 plain.replace(b':', b':c')
146 .replace(b',', b':o')
146 .replace(b',', b':o')
147 .replace(b';', b':s')
147 .replace(b';', b':s')
148 .replace(b'=', b':e')
148 .replace(b'=', b':e')
149 )
149 )
150
150
151
151
152 def unescapebatcharg(escaped):
152 def unescapebatcharg(escaped):
153 return (
153 return (
154 escaped.replace(b':e', b'=')
154 escaped.replace(b':e', b'=')
155 .replace(b':s', b';')
155 .replace(b':s', b';')
156 .replace(b':o', b',')
156 .replace(b':o', b',')
157 .replace(b':c', b':')
157 .replace(b':c', b':')
158 )
158 )
159
159
160
160
161 # mapping of options accepted by getbundle and their types
161 # mapping of options accepted by getbundle and their types
162 #
162 #
163 # Meant to be extended by extensions. It is the extension's responsibility to
163 # Meant to be extended by extensions. It is the extension's responsibility to
164 # ensure such options are properly processed in exchange.getbundle.
164 # ensure such options are properly processed in exchange.getbundle.
165 #
165 #
166 # supported types are:
166 # supported types are:
167 #
167 #
168 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
168 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
169 # :csv: list of values, transmitted as comma-separated values
169 # :csv: list of values, transmitted as comma-separated values
170 # :scsv: set of values, transmitted as comma-separated values
170 # :scsv: set of values, transmitted as comma-separated values
171 # :plain: string with no transformation needed.
171 # :plain: string with no transformation needed.
172 GETBUNDLE_ARGUMENTS = {
172 GETBUNDLE_ARGUMENTS = {
173 b'heads': b'nodes',
173 b'heads': b'nodes',
174 b'bookmarks': b'boolean',
174 b'bookmarks': b'boolean',
175 b'common': b'nodes',
175 b'common': b'nodes',
176 b'obsmarkers': b'boolean',
176 b'obsmarkers': b'boolean',
177 b'phases': b'boolean',
177 b'phases': b'boolean',
178 b'bundlecaps': b'scsv',
178 b'bundlecaps': b'scsv',
179 b'listkeys': b'csv',
179 b'listkeys': b'csv',
180 b'cg': b'boolean',
180 b'cg': b'boolean',
181 b'cbattempted': b'boolean',
181 b'cbattempted': b'boolean',
182 b'stream': b'boolean',
182 b'stream': b'boolean',
183 b'includepats': b'csv',
183 b'includepats': b'csv',
184 b'excludepats': b'csv',
184 b'excludepats': b'csv',
185 }
185 }
186
186
187
187
188 class baseprotocolhandler(Protocol):
188 class baseprotocolhandler(Protocol):
189 """Abstract base class for wire protocol handlers.
189 """Abstract base class for wire protocol handlers.
190
190
191 A wire protocol handler serves as an interface between protocol command
191 A wire protocol handler serves as an interface between protocol command
192 handlers and the wire protocol transport layer. Protocol handlers provide
192 handlers and the wire protocol transport layer. Protocol handlers provide
193 methods to read command arguments, redirect stdio for the duration of
193 methods to read command arguments, redirect stdio for the duration of
194 the request, handle response types, etc.
194 the request, handle response types, etc.
195 """
195 """
196
196
197 @property
197 @property
198 @abc.abstractmethod
198 @abc.abstractmethod
199 def name(self) -> bytes:
199 def name(self) -> bytes:
200 """The name of the protocol implementation.
200 """The name of the protocol implementation.
201
201
202 Used for uniquely identifying the transport type.
202 Used for uniquely identifying the transport type.
203 """
203 """
204
204
205 @abc.abstractmethod
205 def getargs(self, args):
206 def getargs(self, args):
206 """return the value for arguments in <args>
207 """return the value for arguments in <args>
207
208
208 For version 1 transports, returns a list of values in the same
209 For version 1 transports, returns a list of values in the same
209 order they appear in ``args``. For version 2 transports, returns
210 order they appear in ``args``. For version 2 transports, returns
210 a dict mapping argument name to value.
211 a dict mapping argument name to value.
211 """
212 """
212
213
214 @abc.abstractmethod
213 def getprotocaps(self):
215 def getprotocaps(self):
214 """Returns the list of protocol-level capabilities of client
216 """Returns the list of protocol-level capabilities of client
215
217
216 Returns a list of capabilities as declared by the client for
218 Returns a list of capabilities as declared by the client for
217 the current request (or connection for stateful protocol handlers)."""
219 the current request (or connection for stateful protocol handlers)."""
218
220
221 @abc.abstractmethod
219 def getpayload(self):
222 def getpayload(self):
220 """Provide a generator for the raw payload.
223 """Provide a generator for the raw payload.
221
224
222 The caller is responsible for ensuring that the full payload is
225 The caller is responsible for ensuring that the full payload is
223 processed.
226 processed.
224 """
227 """
225
228
229 @abc.abstractmethod
226 def mayberedirectstdio(self):
230 def mayberedirectstdio(self):
227 """Context manager to possibly redirect stdio.
231 """Context manager to possibly redirect stdio.
228
232
229 The context manager yields a file-object like object that receives
233 The context manager yields a file-object like object that receives
230 stdout and stderr output when the context manager is active. Or it
234 stdout and stderr output when the context manager is active. Or it
231 yields ``None`` if no I/O redirection occurs.
235 yields ``None`` if no I/O redirection occurs.
232
236
233 The intent of this context manager is to capture stdio output
237 The intent of this context manager is to capture stdio output
234 so it may be sent in the response. Some transports support streaming
238 so it may be sent in the response. Some transports support streaming
235 stdio to the client in real time. For these transports, stdio output
239 stdio to the client in real time. For these transports, stdio output
236 won't be captured.
240 won't be captured.
237 """
241 """
238
242
243 @abc.abstractmethod
239 def client(self):
244 def client(self):
240 """Returns a string representation of this client (as bytes)."""
245 """Returns a string representation of this client (as bytes)."""
241
246
247 @abc.abstractmethod
242 def addcapabilities(self, repo, caps):
248 def addcapabilities(self, repo, caps):
243 """Adds advertised capabilities specific to this protocol.
249 """Adds advertised capabilities specific to this protocol.
244
250
245 Receives the list of capabilities collected so far.
251 Receives the list of capabilities collected so far.
246
252
247 Returns a list of capabilities. The passed in argument can be returned.
253 Returns a list of capabilities. The passed in argument can be returned.
248 """
254 """
249
255
256 @abc.abstractmethod
250 def checkperm(self, perm):
257 def checkperm(self, perm):
251 """Validate that the client has permissions to perform a request.
258 """Validate that the client has permissions to perform a request.
252
259
253 The argument is the permission required to proceed. If the client
260 The argument is the permission required to proceed. If the client
254 doesn't have that permission, the exception should raise or abort
261 doesn't have that permission, the exception should raise or abort
255 in a protocol specific manner.
262 in a protocol specific manner.
256 """
263 """
257
264
258
265
259 class commandentry:
266 class commandentry:
260 """Represents a declared wire protocol command."""
267 """Represents a declared wire protocol command."""
261
268
262 def __init__(
269 def __init__(
263 self,
270 self,
264 func,
271 func,
265 args=b'',
272 args=b'',
266 transports=None,
273 transports=None,
267 permission=b'push',
274 permission=b'push',
268 cachekeyfn=None,
275 cachekeyfn=None,
269 extracapabilitiesfn=None,
276 extracapabilitiesfn=None,
270 ):
277 ):
271 self.func = func
278 self.func = func
272 self.args = args
279 self.args = args
273 self.transports = transports or set()
280 self.transports = transports or set()
274 self.permission = permission
281 self.permission = permission
275 self.cachekeyfn = cachekeyfn
282 self.cachekeyfn = cachekeyfn
276 self.extracapabilitiesfn = extracapabilitiesfn
283 self.extracapabilitiesfn = extracapabilitiesfn
277
284
278 def _merge(self, func, args):
285 def _merge(self, func, args):
279 """Merge this instance with an incoming 2-tuple.
286 """Merge this instance with an incoming 2-tuple.
280
287
281 This is called when a caller using the old 2-tuple API attempts
288 This is called when a caller using the old 2-tuple API attempts
282 to replace an instance. The incoming values are merged with
289 to replace an instance. The incoming values are merged with
283 data not captured by the 2-tuple and a new instance containing
290 data not captured by the 2-tuple and a new instance containing
284 the union of the two objects is returned.
291 the union of the two objects is returned.
285 """
292 """
286 return commandentry(
293 return commandentry(
287 func,
294 func,
288 args=args,
295 args=args,
289 transports=set(self.transports),
296 transports=set(self.transports),
290 permission=self.permission,
297 permission=self.permission,
291 )
298 )
292
299
293 # Old code treats instances as 2-tuples. So expose that interface.
300 # Old code treats instances as 2-tuples. So expose that interface.
294 def __iter__(self):
301 def __iter__(self):
295 yield self.func
302 yield self.func
296 yield self.args
303 yield self.args
297
304
298 def __getitem__(self, i):
305 def __getitem__(self, i):
299 if i == 0:
306 if i == 0:
300 return self.func
307 return self.func
301 elif i == 1:
308 elif i == 1:
302 return self.args
309 return self.args
303 else:
310 else:
304 raise IndexError(b'can only access elements 0 and 1')
311 raise IndexError(b'can only access elements 0 and 1')
305
312
306
313
307 class commanddict(dict):
314 class commanddict(dict):
308 """Container for registered wire protocol commands.
315 """Container for registered wire protocol commands.
309
316
310 It behaves like a dict. But __setitem__ is overwritten to allow silent
317 It behaves like a dict. But __setitem__ is overwritten to allow silent
311 coercion of values from 2-tuples for API compatibility.
318 coercion of values from 2-tuples for API compatibility.
312 """
319 """
313
320
314 def __setitem__(self, k, v):
321 def __setitem__(self, k, v):
315 if isinstance(v, commandentry):
322 if isinstance(v, commandentry):
316 pass
323 pass
317 # Cast 2-tuples to commandentry instances.
324 # Cast 2-tuples to commandentry instances.
318 elif isinstance(v, tuple):
325 elif isinstance(v, tuple):
319 if len(v) != 2:
326 if len(v) != 2:
320 raise ValueError(b'command tuples must have exactly 2 elements')
327 raise ValueError(b'command tuples must have exactly 2 elements')
321
328
322 # It is common for extensions to wrap wire protocol commands via
329 # It is common for extensions to wrap wire protocol commands via
323 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
330 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
324 # doing this aren't aware of the new API that uses objects to store
331 # doing this aren't aware of the new API that uses objects to store
325 # command entries, we automatically merge old state with new.
332 # command entries, we automatically merge old state with new.
326 if k in self:
333 if k in self:
327 v = self[k]._merge(v[0], v[1])
334 v = self[k]._merge(v[0], v[1])
328 else:
335 else:
329 # Use default values from @wireprotocommand.
336 # Use default values from @wireprotocommand.
330 v = commandentry(
337 v = commandentry(
331 v[0],
338 v[0],
332 args=v[1],
339 args=v[1],
333 transports=set(TRANSPORTS),
340 transports=set(TRANSPORTS),
334 permission=b'push',
341 permission=b'push',
335 )
342 )
336 else:
343 else:
337 raise ValueError(
344 raise ValueError(
338 b'command entries must be commandentry instances '
345 b'command entries must be commandentry instances '
339 b'or 2-tuples'
346 b'or 2-tuples'
340 )
347 )
341
348
342 return super(commanddict, self).__setitem__(k, v)
349 return super(commanddict, self).__setitem__(k, v)
343
350
344 def commandavailable(self, command, proto):
351 def commandavailable(self, command, proto):
345 """Determine if a command is available for the requested protocol."""
352 """Determine if a command is available for the requested protocol."""
346 assert proto.name in TRANSPORTS
353 assert proto.name in TRANSPORTS
347
354
348 entry = self.get(command)
355 entry = self.get(command)
349
356
350 if not entry:
357 if not entry:
351 return False
358 return False
352
359
353 if proto.name not in entry.transports:
360 if proto.name not in entry.transports:
354 return False
361 return False
355
362
356 return True
363 return True
357
364
358
365
359 def supportedcompengines(ui, role):
366 def supportedcompengines(ui, role):
360 """Obtain the list of supported compression engines for a request."""
367 """Obtain the list of supported compression engines for a request."""
361 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
368 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
362
369
363 compengines = compression.compengines.supportedwireengines(role)
370 compengines = compression.compengines.supportedwireengines(role)
364
371
365 # Allow config to override default list and ordering.
372 # Allow config to override default list and ordering.
366 if role == compression.SERVERROLE:
373 if role == compression.SERVERROLE:
367 configengines = ui.configlist(b'server', b'compressionengines')
374 configengines = ui.configlist(b'server', b'compressionengines')
368 config = b'server.compressionengines'
375 config = b'server.compressionengines'
369 else:
376 else:
370 # This is currently implemented mainly to facilitate testing. In most
377 # This is currently implemented mainly to facilitate testing. In most
371 # cases, the server should be in charge of choosing a compression engine
378 # cases, the server should be in charge of choosing a compression engine
372 # because a server has the most to lose from a sub-optimal choice. (e.g.
379 # because a server has the most to lose from a sub-optimal choice. (e.g.
373 # CPU DoS due to an expensive engine or a network DoS due to poor
380 # CPU DoS due to an expensive engine or a network DoS due to poor
374 # compression ratio).
381 # compression ratio).
375 configengines = ui.configlist(
382 configengines = ui.configlist(
376 b'experimental', b'clientcompressionengines'
383 b'experimental', b'clientcompressionengines'
377 )
384 )
378 config = b'experimental.clientcompressionengines'
385 config = b'experimental.clientcompressionengines'
379
386
380 # No explicit config. Filter out the ones that aren't supposed to be
387 # No explicit config. Filter out the ones that aren't supposed to be
381 # advertised and return default ordering.
388 # advertised and return default ordering.
382 if not configengines:
389 if not configengines:
383 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
390 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
384 return [
391 return [
385 e for e in compengines if getattr(e.wireprotosupport(), attr) > 0
392 e for e in compengines if getattr(e.wireprotosupport(), attr) > 0
386 ]
393 ]
387
394
388 # If compression engines are listed in the config, assume there is a good
395 # If compression engines are listed in the config, assume there is a good
389 # reason for it (like server operators wanting to achieve specific
396 # reason for it (like server operators wanting to achieve specific
390 # performance characteristics). So fail fast if the config references
397 # performance characteristics). So fail fast if the config references
391 # unusable compression engines.
398 # unusable compression engines.
392 validnames = {e.name() for e in compengines}
399 validnames = {e.name() for e in compengines}
393 invalidnames = {e for e in configengines if e not in validnames}
400 invalidnames = {e for e in configengines if e not in validnames}
394 if invalidnames:
401 if invalidnames:
395 raise error.Abort(
402 raise error.Abort(
396 _(b'invalid compression engine defined in %s: %s')
403 _(b'invalid compression engine defined in %s: %s')
397 % (config, b', '.join(sorted(invalidnames)))
404 % (config, b', '.join(sorted(invalidnames)))
398 )
405 )
399
406
400 compengines = [e for e in compengines if e.name() in configengines]
407 compengines = [e for e in compengines if e.name() in configengines]
401 compengines = sorted(
408 compengines = sorted(
402 compengines, key=lambda e: configengines.index(e.name())
409 compengines, key=lambda e: configengines.index(e.name())
403 )
410 )
404
411
405 if not compengines:
412 if not compengines:
406 raise error.Abort(
413 raise error.Abort(
407 _(
414 _(
408 b'%s config option does not specify any known '
415 b'%s config option does not specify any known '
409 b'compression engines'
416 b'compression engines'
410 )
417 )
411 % config,
418 % config,
412 hint=_(b'usable compression engines: %s')
419 hint=_(b'usable compression engines: %s')
413 % b', '.sorted(validnames), # pytype: disable=attribute-error
420 % b', '.sorted(validnames), # pytype: disable=attribute-error
414 )
421 )
415
422
416 return compengines
423 return compengines
417
424
418
425
419 @attr.s
426 @attr.s
420 class encodedresponse:
427 class encodedresponse:
421 """Represents response data that is already content encoded.
428 """Represents response data that is already content encoded.
422
429
423 Wire protocol version 2 only.
430 Wire protocol version 2 only.
424
431
425 Commands typically emit Python objects that are encoded and sent over the
432 Commands typically emit Python objects that are encoded and sent over the
426 wire. If commands emit an object of this type, the encoding step is bypassed
433 wire. If commands emit an object of this type, the encoding step is bypassed
427 and the content from this object is used instead.
434 and the content from this object is used instead.
428 """
435 """
429
436
430 data = attr.ib()
437 data = attr.ib()
431
438
432
439
433 @attr.s
440 @attr.s
434 class alternatelocationresponse:
441 class alternatelocationresponse:
435 """Represents a response available at an alternate location.
442 """Represents a response available at an alternate location.
436
443
437 Instances are sent in place of actual response objects when the server
444 Instances are sent in place of actual response objects when the server
438 is sending a "content redirect" response.
445 is sending a "content redirect" response.
439
446
440 Only compatible with wire protocol version 2.
447 Only compatible with wire protocol version 2.
441 """
448 """
442
449
443 url = attr.ib()
450 url = attr.ib()
444 mediatype = attr.ib()
451 mediatype = attr.ib()
445 size = attr.ib(default=None)
452 size = attr.ib(default=None)
446 fullhashes = attr.ib(default=None)
453 fullhashes = attr.ib(default=None)
447 fullhashseed = attr.ib(default=None)
454 fullhashseed = attr.ib(default=None)
448 serverdercerts = attr.ib(default=None)
455 serverdercerts = attr.ib(default=None)
449 servercadercerts = attr.ib(default=None)
456 servercadercerts = attr.ib(default=None)
450
457
451
458
452 @attr.s
459 @attr.s
453 class indefinitebytestringresponse:
460 class indefinitebytestringresponse:
454 """Represents an object to be encoded to an indefinite length bytestring.
461 """Represents an object to be encoded to an indefinite length bytestring.
455
462
456 Instances are initialized from an iterable of chunks, with each chunk being
463 Instances are initialized from an iterable of chunks, with each chunk being
457 a bytes instance.
464 a bytes instance.
458 """
465 """
459
466
460 chunks = attr.ib()
467 chunks = attr.ib()
General Comments 0
You need to be logged in to leave comments. Login now