Coverage for src/gitlabracadabra/containers/registry_session.py: 86%

37 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-10 17:02 +0100

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 

17from __future__ import annotations 

18 

19from typing import TYPE_CHECKING 

20 

21from gitlabracadabra.containers.authenticated_session import AuthenticatedSession 

22from gitlabracadabra.containers.const import DOCKER_HOSTNAME, DOCKER_REGISTRY 

23 

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

25 from collections.abc import Callable 

26 

27 from requests import Response 

28 from requests.auth import AuthBase 

29 

30 from gitlabracadabra.containers.authenticated_session import Data, Params 

31 from gitlabracadabra.containers.scope import Scope 

32 

33 

34class RegistrySession: 

35 """Container registry HTTP methods.""" 

36 

37 def __init__( 

38 self, 

39 hostname: str, 

40 session_callback: Callable[[AuthenticatedSession], None] | None = None, 

41 ) -> None: 

42 """Instantiate a registry connection. 

43 

44 Args: 

45 hostname: fqdn of a registry. 

46 session_callback: Callback applied to requests Session. 

47 """ 

48 self._session = AuthenticatedSession() 

49 self._hostname = hostname 

50 if hostname == DOCKER_HOSTNAME: 

51 self._session.connection_hostname = DOCKER_REGISTRY 

52 else: 

53 self._session.connection_hostname = hostname 

54 if session_callback is not None: 

55 session_callback(self._session) 

56 # Cache where blobs are present 

57 # dict key is digest, value is a list of manifest names 

58 # Used in WithBlobs 

59 self._blobs: dict[str, list[str]] = {} 

60 self._sizes: dict[str, int] = {} 

61 

62 def __del__(self) -> None: 

63 """Destroy a registry connection.""" 

64 self._session.close() 

65 

66 @property 

67 def hostname(self) -> str: 

68 """Get hostname. 

69 

70 Returns: 

71 The registry hostname. 

72 """ 

73 return self._hostname 

74 

75 def request( 

76 self, 

77 method: str, 

78 url: str, 

79 *, 

80 scopes: set[Scope] | None = None, 

81 params: Params = None, 

82 data: Data | None = None, 

83 headers: dict[str, str] | None = None, 

84 content_type: str | None = None, 

85 accept: tuple[str, ...] | None = None, 

86 auth: AuthBase | None = None, 

87 stream: bool | None = None, 

88 raise_for_status: bool = True, 

89 ) -> Response: 

90 """Send an HTTP request. 

91 

92 Args: 

93 method: HTTP method. 

94 url: Either a path or a full url. 

95 scopes: An optional set of scopes. 

96 params: query string params. 

97 data: Request body stream. 

98 headers: Request headers. 

99 content_type: Uploaded MIME type. 

100 accept: An optional list of accepted mime-types. 

101 auth: HTTPBasicAuth. 

102 stream: Stream the response. 

103 raise_for_status: Raises `requests.HTTPError`, if one occurred. 

104 

105 Returns: 

106 A Response. 

107 """ 

108 headers = headers.copy() if headers else {} 

109 if accept: 

110 headers["Accept"] = ", ".join(accept) 

111 if content_type: 

112 headers["Content-Type"] = content_type 

113 

114 self._session.connect(scopes) 

115 response = self._session.authenticated_request( 

116 method, 

117 url, 

118 params=params, 

119 data=data, 

120 headers=headers, 

121 auth=auth, 

122 stream=stream, 

123 ) 

124 if raise_for_status: 124 ↛ 126line 124 didn't jump to line 126 because the condition on line 124 was always true

125 response.raise_for_status() 

126 return response