1#

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>

3#

4# This program is free software: you can redistribute it and/or modify

5# it under the terms of the GNU Lesser General Public License as published by

6# the Free Software Foundation, either version 3 of the License, or

7# (at your option) any later version.

8#

9# This program is distributed in the hope that it will be useful,

10# but WITHOUT ANY WARRANTY; without even the implied warranty of

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

12# GNU Lesser General Public License for more details.

13#

14# You should have received a copy of the GNU Lesser General Public License

15# along with this program. If not, see <http://www.gnu.org/licenses/>.

16

17 from __future__ import annotations

18

19 from typing import TYPE_CHECKING, AnyStr

20

21 if TYPE_CHECKING: 21 ↛ 22 line 21 didn't jump to line 22 because the condition on line 21 was never true

22 from collections.abc import Iterator

23

24 from requests.models import Response

25

26

27 class Stream:

28 """Stream."""

29

30 def __init__(self, response: Response, chunksize: int = 65536) -> None:

31 """Initialize Stream.

32

33 Args:

34 response: Streamed response.

35 chunksize: Chunk size (used when there is no Content-Length header).

36 """

37 self._response = response

38 self._chunksize = chunksize

39

40 def __bool__(self) -> bool:

41 """Stream as boolean.

42

43 Needed for Session.request() which uses: data=data or dict().

44 (otherwise, would be considered False when length is 0).

45

46 Returns:

47 Always True.

48 """

49 return True

50

51 def __len__(self) -> int:

52 """Get stream length.

53

54 Returns:

55 The stream length. Zero if there is no Content-Length header.

56 """

57 return int(self._response.headers.get("Content-Length", "0"))

58

59 def __iter__(self) -> Iterator[bytes]:

60 """Get an iterator of chunks of body.

61

62 Returns:

63 A bytes iterator.

64 """

65 return self._response.raw.stream(self._chunksize)

66

67 @property

68 def name(self) -> str:

69 """Return URL.

70

71 This is needed to have proper file name in multipart upload.

72

73 Called from requests.utils.guess_filename(),

74 called from requests.models.RequestEncodingMixin._encode_files(),

75 called from requests.models.PreparedRequest.prepare_body().

76

77 Returns:

78 The response URL.

79 """

80 if self._response.history: 80 ↛ 82 line 80 didn't jump to line 82 because the condition on line 80 was never true

81 # Keep original request URL, to avoid too long filename

82 return self._response.history[0].url

83 return self._response.url

84

85 def read(self, size: int | None = None) -> AnyStr: # type: ignore

86 """Read stream.

87

88 Args:

89 size: Length to read. Defaulting to None like http.client.HTTPResponse.read().

90

91 Returns:

92 The read bytes/str.

93 """

94 return self._response.raw.read(size) # type: ignore