Coverage for src/rsnapshot_docker_compose_backup/utils/docker_api_client.py: 76%
80 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-07 01:03 +0200
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-07 01:03 +0200
1from dataclasses import dataclass
2import json
3import socket
4from typing import Any, Dict, List, Optional
5import urllib.parse
8@dataclass
9class HttpResponse:
10 protocol_version: str
11 status_code: int
12 status_text: str
13 headers: Dict[str, str]
14 json_body: Any
17class Api:
19 _docker_socket: Optional[socket.socket] = None
21 def __init__(
22 self, socket_connection: str = "unix:///run/docker.sock", version: str = "v1.46"
23 ) -> None:
24 self.docker_socket = self._open_socket(socket_connection)
25 self.version = version
27 def _open_socket(self, socket_connection: str) -> socket.socket:
28 if socket_connection.startswith("unix://"):
29 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
30 sock.connect(socket_connection[7:]) # Remove 'unix://'
31 return sock
32 if socket_connection.startswith("http://"):
33 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34 parts = socket_connection[7:].split(":") # Remove 'unix://'
35 host = parts[0]
36 port = 80
37 if len(parts) == 2:
38 port = int(parts[1])
39 print(f"connect to {host}, {port}")
40 sock.connect((host, port))
41 return sock
42 raise ValueError("Only Unix and http Sockets are supported")
44 def get(
45 self,
46 endpoint: str,
47 *,
48 query_parameter: Optional[Dict[str, str]] = None,
49 header: Optional[Dict[str, str]] = None,
50 ) -> HttpResponse:
51 path = f"/{self.version}{endpoint}"
52 if query_parameter is not None:
53 parameter: List[str] = []
54 for name, value in query_parameter.items():
55 parameter.append(f"{name}={urllib.parse.quote_plus(value)}")
56 path = path + "?" + "&".join(parameter)
57 request = [
58 f"GET {path} HTTP/1.1",
59 "Host:docker.sock",
60 ]
61 if header is not None:
62 for name, value in header.items():
63 request.append(f"{name}:{value}")
64 request.append("\r\n")
65 self.docker_socket.send("\r\n".join(request).encode("utf-8"))
66 return self._parse_response(self.docker_socket)
68 @staticmethod
69 def _parse_response(sock: socket.socket) -> HttpResponse:
70 # Read statusline and headers. I read byte by byte to be sure that not too much is read wich could lead to blocking
71 message_start = b""
72 while not message_start.endswith(b"\r\n\r\n"):
73 message_start = message_start + sock.recv(1)
74 lines: List[str] = message_start.decode("utf-8").splitlines()
75 # Parse statusline
76 status_line = lines[0]
77 protocol_version = status_line.split(" ")[0]
78 status_code = int(status_line.split(" ")[1])
79 status_text = " ".join(status_line.split(" ")[2:])
80 if status_code >= 400:
81 raise ValueError(status_text)
82 # Read headers
83 headers: Dict[str, str] = {}
84 for line in lines[1:]:
85 split = line.split(":")
86 if len(split) == 2:
87 headers[split[0]] = split[1].strip()
88 # read body
89 if "Content-Length" in headers:
90 body_length = int(headers["Content-Length"])
91 body = sock.recv(body_length).decode("utf-8")
92 elif (
93 "Transfer-Encoding" in headers and headers["Transfer-Encoding"] == "chunked"
94 ):
95 body = ""
96 while True:
97 chunk = b""
98 while not chunk.endswith(b"\r\n"):
99 chunk += sock.recv(1)
100 length = int(chunk.decode("utf-8"), 16)
101 if length == 0:
102 break
103 body = body + sock.recv(length).decode("utf-8")
104 sock.recv(2) # Skip ending \r\n
105 else:
106 raise ValueError(headers)
107 return HttpResponse(
108 protocol_version=protocol_version,
109 status_code=status_code,
110 status_text=status_text,
111 headers=headers,
112 json_body=json.loads(body),
113 )