gnetclisdk 0.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ global-exclude .git*
2
+ global-exclude .ipynb_checkpoints
3
+ global-exclude *.py[co]
4
+ global-exclude __pycache__/**
5
+
6
+ include requirements.txt
7
+ include README.md
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.1
2
+ Name: gnetclisdk
3
+ Version: 0.0
4
+ Summary: Client for Gnetcli GRPC-server
5
+ Home-page: https://github.com/annetutil/gnetcli
6
+ Author: Alexander Balezin
7
+ Author-email: gescheit12@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: protobuf==4.24.4
17
+ Requires-Dist: grpcio==1.59.2
18
+ Requires-Dist: googleapis-common-protos==1.61.0
19
+
20
+ ## Python client for Gnetcli GRPC server
21
+
22
+ Gnetcli provides a universal way to execute arbitrary commands using a CLI,
23
+ eliminating the need for screen scraping with expect.
24
+
25
+ See documentation on [gnetcli server](https://annetutil.github.io/gnetcli/).
26
+
27
+ Example:
28
+
29
+ ```python
30
+ from gnetclisdk.client import Credentials, Gnetcli
31
+ import os, asyncio
32
+
33
+ async def example():
34
+ api = Gnetcli(insecure_grpc=True)
35
+ dev_creds = Credentials(os.environ.get("LOGIN"), os.environ.get("PASSWORD"))
36
+ res = await api.cmd(hostname="myhost", device="huawei", cmd="dis clock", credentials=dev_creds)
37
+ print("err=%s status=%s out=%s" % (res.error, res.status, res.out))
38
+
39
+ asyncio.run(example())
40
+ ```
41
+
42
+ Output:
43
+ ```
44
+ err=b'' status=0 out=b'2023-11-10 09:31:58\nFriday\nTime Zone(UTC) : UTC'
45
+ ```
@@ -0,0 +1,26 @@
1
+ ## Python client for Gnetcli GRPC server
2
+
3
+ Gnetcli provides a universal way to execute arbitrary commands using a CLI,
4
+ eliminating the need for screen scraping with expect.
5
+
6
+ See documentation on [gnetcli server](https://annetutil.github.io/gnetcli/).
7
+
8
+ Example:
9
+
10
+ ```python
11
+ from gnetclisdk.client import Credentials, Gnetcli
12
+ import os, asyncio
13
+
14
+ async def example():
15
+ api = Gnetcli(insecure_grpc=True)
16
+ dev_creds = Credentials(os.environ.get("LOGIN"), os.environ.get("PASSWORD"))
17
+ res = await api.cmd(hostname="myhost", device="huawei", cmd="dis clock", credentials=dev_creds)
18
+ print("err=%s status=%s out=%s" % (res.error, res.status, res.out))
19
+
20
+ asyncio.run(example())
21
+ ```
22
+
23
+ Output:
24
+ ```
25
+ err=b'' status=0 out=b'2023-11-10 09:31:58\nFriday\nTime Zone(UTC) : UTC'
26
+ ```
@@ -0,0 +1,41 @@
1
+ import abc
2
+
3
+
4
+ class ClientAuthentication(abc.ABC):
5
+ @abc.abstractmethod
6
+ def get_authentication_header_key(self) -> str:
7
+ """
8
+ Name of the header used for authentication.
9
+ :return: Header name.
10
+ """
11
+ raise NotImplementedError("abstract method get_authentication_header_key() not implemented")
12
+
13
+ @abc.abstractmethod
14
+ def create_authentication_header_value(self) -> str:
15
+ """
16
+ Creates value for authentication header.
17
+ :return: Authentication header value.
18
+ """
19
+ raise NotImplementedError("abstract method create_authentication_header_value() not implemented")
20
+
21
+
22
+ class OAuthClientAuthentication(ClientAuthentication):
23
+ def __init__(self, token: str):
24
+ self.__token = token
25
+
26
+ def get_authentication_header_key(self) -> str:
27
+ return "authorization"
28
+
29
+ def create_authentication_header_value(self) -> str:
30
+ return f"OAuth {self.__token}"
31
+
32
+
33
+ class BasicClientAuthentication(ClientAuthentication):
34
+ def __init__(self, token: str):
35
+ self.__token = token
36
+
37
+ def get_authentication_header_key(self) -> str:
38
+ return "authorization"
39
+
40
+ def create_authentication_header_value(self) -> str:
41
+ return f"Basic {self.__token}"
@@ -0,0 +1,481 @@
1
+ import asyncio
2
+ import logging
3
+ import os.path
4
+ import uuid
5
+ from abc import ABC, abstractmethod
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass, field
8
+ from functools import partial
9
+ from typing import Any, AsyncIterator, List, Optional, Tuple, Dict
10
+
11
+ import grpc
12
+ from google.protobuf.message import Message
13
+
14
+ from .proto import server_pb2, server_pb2_grpc
15
+ from .auth import BasicClientAuthentication, ClientAuthentication, OAuthClientAuthentication
16
+ from .exceptions import parse_grpc_error
17
+ from .interceptors import get_auth_client_interceptors
18
+
19
+ _logger = logging.getLogger(__name__)
20
+ HEADER_REQUEST_ID = "x-request-id"
21
+ HEADER_USER_AGENT = "user-agent"
22
+ DEFAULT_USER_AGENT = "Gnetcli SDK"
23
+ DEFAULT_SERVER = "localhost:50051"
24
+ SERVER_ENV = "GNETCLI_SERVER"
25
+ GRPC_MAX_MESSAGE_LENGTH = 130 * 1024**2
26
+
27
+ default_grpc_options: List[Tuple[str, Any]] = [
28
+ ("grpc.max_concurrent_streams", 900),
29
+ ("grpc.max_send_message_length", GRPC_MAX_MESSAGE_LENGTH),
30
+ ("grpc.max_receive_message_length", GRPC_MAX_MESSAGE_LENGTH),
31
+ ]
32
+
33
+
34
+ @dataclass
35
+ class QA:
36
+ question: str
37
+ answer: str
38
+
39
+
40
+ @dataclass
41
+ class Credentials:
42
+ login: str
43
+ password: str
44
+
45
+ def make_pb(self) -> Message:
46
+ pb = server_pb2.Credentials()
47
+ pb.login = self.login
48
+ pb.password = self.password
49
+ return pb
50
+
51
+
52
+ @dataclass
53
+ class File:
54
+ content: bytes
55
+ status: server_pb2.FileStatus
56
+
57
+
58
+ @dataclass
59
+ class HostParams:
60
+ device: str
61
+ port: Optional[int] = None
62
+ hostname: Optional[str] = None
63
+ credentials: Optional[Credentials] = None
64
+ ip: Optional[str] = None
65
+
66
+ def make_pb(self) -> Message:
67
+ pbcmd = server_pb2.HostParams(
68
+ host=self.hostname,
69
+ port=self.port,
70
+ credentials=self.credentials.make_pb(),
71
+ device=self.device,
72
+ ip=self.ip,
73
+ )
74
+ return pbcmd
75
+
76
+
77
+ def make_auth(auth_token: str) -> ClientAuthentication:
78
+ if auth_token.lower().startswith("oauth"):
79
+ authentication = OAuthClientAuthentication(auth_token.split(" ")[1])
80
+ elif auth_token.lower().startswith("basic"):
81
+ authentication = BasicClientAuthentication(auth_token.split(" ")[1])
82
+ else:
83
+ raise Exception("unknown token type")
84
+ return authentication
85
+
86
+
87
+ class Gnetcli:
88
+ def __init__(
89
+ self,
90
+ auth_token: Optional[str] = None, # like 'Basic ...'
91
+ server: Optional[str] = None,
92
+ target_name_override: Optional[str] = None,
93
+ cert_file: Optional[str] = None,
94
+ user_agent: str = DEFAULT_USER_AGENT,
95
+ insecure_grpc: bool = False,
96
+ ):
97
+ if server is None:
98
+ self._server = os.getenv(SERVER_ENV, DEFAULT_SERVER)
99
+ else:
100
+ self._server = server
101
+ self._user_agent = user_agent
102
+
103
+ options: List[Tuple[str, Any]] = [
104
+ *default_grpc_options,
105
+ ("grpc.primary_user_agent", user_agent),
106
+ ]
107
+ if target_name_override:
108
+ _logger.warning("set target_name_override %s", target_name_override)
109
+ options.append(("grpc.ssl_target_name_override", target_name_override))
110
+ self._target_name_override = target_name_override
111
+ cert = get_cert(cert_file=cert_file)
112
+ channel_credentials = grpc.ssl_channel_credentials(root_certificates=cert)
113
+ interceptors = []
114
+ if auth_token:
115
+ authentication: ClientAuthentication
116
+ authentication = make_auth(auth_token)
117
+ interceptors = get_auth_client_interceptors(authentication)
118
+ grpc_channel_fn = partial(grpc.aio.secure_channel, credentials=channel_credentials, interceptors=interceptors)
119
+ if insecure_grpc:
120
+ grpc_channel_fn = partial(grpc.aio.insecure_channel, interceptors=interceptors)
121
+ self._grpc_channel_fn = grpc_channel_fn
122
+ self._options = options
123
+ self._channel: Optional[grpc.aio.Channel] = None
124
+ self._insecure_grpc: bool = insecure_grpc
125
+
126
+ async def cmd(
127
+ self,
128
+ hostname: str,
129
+ cmd: str,
130
+ trace: bool = False,
131
+ qa: Optional[List[QA]] = None,
132
+ read_timeout: float = 0.0,
133
+ cmd_timeout: float = 0.0,
134
+ host_params: Optional[HostParams] = None,
135
+ ) -> Message:
136
+ pbcmd = make_cmd(
137
+ hostname=hostname,
138
+ cmd=cmd,
139
+ trace=trace,
140
+ qa=qa,
141
+ read_timeout=read_timeout,
142
+ cmd_timeout=cmd_timeout,
143
+ host_params=host_params,
144
+ )
145
+ if self._channel is None:
146
+ _logger.debug("connect to %s", self._server)
147
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
148
+ stub = server_pb2_grpc.GnetcliStub(self._channel)
149
+ response = await grpc_call_wrapper(stub.Exec, pbcmd)
150
+ return response
151
+
152
+ async def add_device(
153
+ self,
154
+ name: str,
155
+ prompt_expression: str,
156
+ error_expression: Optional[str] = None,
157
+ pager_expression: Optional[str] = None,
158
+ ) -> Message:
159
+ pbdev = server_pb2.Device
160
+ pbdev.name = name
161
+ pbdev.prompt_expression = prompt_expression
162
+ if error_expression:
163
+ pbdev.error_expression = error_expression
164
+ if pager_expression:
165
+ pbdev.pager_expression = pager_expression
166
+ if self._channel is None:
167
+ _logger.debug("connect to %s", self._server)
168
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
169
+ stub = server_pb2_grpc.GnetcliStub(self._channel)
170
+ response = await grpc_call_wrapper(stub.AddDevice, pbdev)
171
+ return response
172
+
173
+ def connect(self) -> None:
174
+ # make connection here will pass it to session
175
+ if not self._channel:
176
+ _logger.debug("real connect to %s", self._server)
177
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
178
+
179
+ async def cmd_netconf(self, hostname: str, cmd: str, json: bool = False, trace: bool = False) -> Message:
180
+ pbcmd = server_pb2.CMDNetconf(host=hostname, cmd=cmd, json=json, trace=trace)
181
+ _logger.debug("connect to %s", self._server)
182
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
183
+ stub = server_pb2_grpc.GnetcliStub(channel)
184
+ _logger.debug("executing netconf cmd: %r", pbcmd)
185
+ try:
186
+ response = await grpc_call_wrapper(stub.ExecNetconf, pbcmd)
187
+ except Exception as e:
188
+ _logger.error("error hostname=%s cmd=%r error=%s", hostname, repr(pbcmd), e)
189
+ raise
190
+ return response
191
+
192
+ @asynccontextmanager
193
+ async def cmd_session(self, hostname: str) -> AsyncIterator["GnetcliSessionCmd"]:
194
+ sess = GnetcliSessionCmd(
195
+ hostname,
196
+ server=self._server,
197
+ channel=self._channel,
198
+ target_name_override=self._target_name_override,
199
+ user_agent=self._user_agent,
200
+ insecure_grpc=self._insecure_grpc,
201
+ )
202
+ await sess.connect()
203
+ try:
204
+ yield sess
205
+ finally:
206
+ await sess.close()
207
+
208
+ @asynccontextmanager
209
+ async def netconf_session(self, hostname: str) -> AsyncIterator["GnetcliSessionNetconf"]:
210
+ sess = GnetcliSessionNetconf(
211
+ hostname,
212
+ # self._token,
213
+ server=self._server,
214
+ target_name_override=self._target_name_override,
215
+ user_agent=self._user_agent,
216
+ insecure_grpc=self._insecure_grpc,
217
+ )
218
+ await sess.connect()
219
+ try:
220
+ yield sess
221
+ finally:
222
+ await sess.close()
223
+
224
+ async def set_host_params(self, hostname: str, params: HostParams) -> None:
225
+ pbcmd = server_pb2.HostParams(
226
+ host=hostname,
227
+ port=params.port,
228
+ credentials=params.credentials.make_pb(),
229
+ device=params.device)
230
+ _logger.debug("connect to %s", self._server)
231
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
232
+ _logger.debug("set params for %s", hostname)
233
+ stub = server_pb2_grpc.GnetcliStub(channel)
234
+ await grpc_call_wrapper(stub.SetupHostParams, pbcmd)
235
+ return
236
+
237
+ async def upload(self, hostname: str, files: Dict[str, File]) -> None:
238
+ pbcmd = server_pb2.FileUploadRequest(host=hostname, files=make_files_request(files))
239
+ _logger.debug("connect to %s", self._server)
240
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
241
+ _logger.debug("upload %s to %s", files.keys(), hostname)
242
+ stub = server_pb2_grpc.GnetcliStub(channel)
243
+ response: Message = await grpc_call_wrapper(stub.Upload, pbcmd)
244
+ _logger.debug("upload res %s", response)
245
+ return
246
+
247
+ async def download(self, hostname: str, paths: List[str]) -> Dict[str, File]:
248
+ pbcmd = server_pb2.FileDownloadRequest(host=hostname, paths=paths)
249
+ _logger.debug("connect to %s", self._server)
250
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
251
+ _logger.debug("download %s from %s", paths, hostname)
252
+ stub = server_pb2_grpc.GnetcliStub(channel)
253
+ response: server_pb2.FilesResult = await grpc_call_wrapper(stub.Download, pbcmd)
254
+ res: Dict[str, File] = {}
255
+ for file in response.files:
256
+ res[file.path] = File(content=file.data, status=file.status)
257
+ return res
258
+
259
+
260
+ class GnetcliSession(ABC):
261
+ def __init__(
262
+ self,
263
+ hostname: str,
264
+ token: str,
265
+ server: str = DEFAULT_SERVER,
266
+ target_name_override: Optional[str] = None,
267
+ cert_file: Optional[str] = None,
268
+ user_agent: str = DEFAULT_USER_AGENT,
269
+ insecure_grpc: bool = False,
270
+ channel: Optional[grpc.aio.Channel] = None,
271
+ credentials: Optional[Credentials] = None,
272
+ ):
273
+ self._hostname = hostname
274
+ self._credentials = credentials
275
+ self._server = server
276
+ self._channel: Optional[grpc.aio.Channel] = channel
277
+ self._stub: Optional[server_pb2_grpc.GnetcliStub] = None
278
+ self._stream: Optional[grpc.aio.StreamStreamCall] = None
279
+ self._user_agent = user_agent
280
+
281
+ options: List[Tuple[str, Any]] = [
282
+ ("grpc.max_concurrent_streams", 900),
283
+ ("grpc.max_send_message_length", GRPC_MAX_MESSAGE_LENGTH),
284
+ ("grpc.max_receive_message_length", GRPC_MAX_MESSAGE_LENGTH),
285
+ ]
286
+ if target_name_override:
287
+ options.append(("grpc.ssl_target_name_override", target_name_override))
288
+ cert = get_cert(cert_file=cert_file)
289
+ channel_credentials = grpc.ssl_channel_credentials(root_certificates=cert)
290
+ authentication: ClientAuthentication
291
+ if token.startswith("OAuth"):
292
+ authentication = OAuthClientAuthentication(token.split(" ")[1])
293
+ elif token.startswith("Basic"):
294
+ authentication = BasicClientAuthentication(token.split(" ")[1])
295
+ else:
296
+ raise Exception("unknown token type")
297
+ interceptors = get_auth_client_interceptors(authentication)
298
+ grpc_channel_fn = partial(grpc.aio.secure_channel, credentials=channel_credentials, interceptors=interceptors)
299
+ if insecure_grpc:
300
+ grpc_channel_fn = partial(grpc.aio.insecure_channel, interceptors=interceptors)
301
+ self._grpc_channel_fn = grpc_channel_fn
302
+ self._options = options
303
+ self._req_id: Optional[Any] = None
304
+
305
+ def _get_metadata(self) -> List[Tuple[str, str]]:
306
+ req_id = make_req_id()
307
+ metadata = [
308
+ (HEADER_REQUEST_ID, req_id),
309
+ (HEADER_USER_AGENT, self._user_agent),
310
+ ]
311
+ return metadata
312
+
313
+ @abstractmethod
314
+ async def connect(self) -> None:
315
+ if self._channel is None:
316
+ _logger.debug("connect to %s self._channel=%s", self._server, self._channel)
317
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
318
+ self._stub = server_pb2_grpc.GnetcliStub(self._channel)
319
+ if self._stub is None:
320
+ raise Exception("empty stub")
321
+
322
+ async def _cmd(self, cmdpb: Any) -> Message:
323
+ # TODO: add connect retry on first cmd
324
+ if not self._stream:
325
+ raise Exception("empty self._stream")
326
+ try:
327
+ _logger.debug("cmd %r on %r", str(cmdpb).replace("\n", ""), self._stream)
328
+ await self._stream.write(cmdpb)
329
+ response: Message = await self._stream.read()
330
+ except grpc.aio.AioRpcError as e:
331
+ _logger.debug("caught exception %s %s", e, parse_grpc_error(e))
332
+ gn_exc, verbose = parse_grpc_error(e)
333
+ last_exc = gn_exc(
334
+ message=f"{e.__class__.__name__} {e.details()}",
335
+ imetadata=e.initial_metadata(), # type: ignore
336
+ verbose=verbose,
337
+ )
338
+ last_exc.__cause__ = e
339
+ raise last_exc from None
340
+ _logger.debug("response %s", format_long_msg(str(response), 100))
341
+ return response
342
+
343
+ async def close(self) -> None:
344
+ _logger.debug("close stream %s", self._stream)
345
+ if self._stream:
346
+ await self._stream.done_writing()
347
+ self._stream.done()
348
+ self._stream = None
349
+
350
+
351
+ class GnetcliSessionCmd(GnetcliSession):
352
+ async def cmd(
353
+ self,
354
+ cmd: str,
355
+ trace: bool = False,
356
+ qa: Optional[List[QA]] = None,
357
+ cmd_timeout: float = 0.0,
358
+ read_timeout: float = 0.0,
359
+ host_params: Optional[HostParams] = None,
360
+ ) -> Message:
361
+ _logger.debug("session cmd %r", cmd)
362
+ pbcmd = make_cmd(
363
+ hostname=self._hostname,
364
+ cmd=cmd,
365
+ trace=trace,
366
+ qa=qa,
367
+ read_timeout=read_timeout,
368
+ cmd_timeout=cmd_timeout,
369
+ host_params=host_params,
370
+ )
371
+ return await self._cmd(pbcmd)
372
+
373
+ async def connect(self) -> None:
374
+ await super(GnetcliSessionCmd, self).connect()
375
+ if self._stub:
376
+ self._stream = self._stub.ExecChat(metadata=self._get_metadata())
377
+ else:
378
+ raise Exception()
379
+
380
+
381
+ class GnetcliSessionNetconf(GnetcliSession):
382
+ async def cmd(self, cmd: str, trace: bool = False, json: bool = False) -> Message:
383
+ _logger.debug("netconf session cmd %r", cmd)
384
+ cmdpb = server_pb2.CMDNetconf(host=self._hostname, cmd=cmd, json=json)
385
+ return await self._cmd(cmdpb)
386
+
387
+ async def connect(self) -> None:
388
+ await super(GnetcliSessionNetconf, self).connect()
389
+ if self._stub:
390
+ self._stream = self._stub.ExecNetconfChat(metadata=self._get_metadata())
391
+ else:
392
+ raise Exception()
393
+
394
+
395
+ async def grpc_call_wrapper(stub: grpc.UnaryUnaryMultiCallable, request: Any) -> Message:
396
+ last_exc: Optional[Exception] = None
397
+ response: Optional[Message] = None
398
+ for i in range(5):
399
+ req_id = make_req_id()
400
+ metadata = [
401
+ (HEADER_REQUEST_ID, req_id),
402
+ ]
403
+ _logger.debug("executing %s: %r, req_id=%s", type(request), repr(request), req_id)
404
+ await asyncio.sleep(i * 2)
405
+ try:
406
+ response = await stub(request=request, metadata=metadata)
407
+ except grpc.aio.AioRpcError as e:
408
+ _logger.debug("caught exception %s req_id=%s %s", e, req_id, parse_grpc_error(e))
409
+ gn_exc, verbose = parse_grpc_error(e)
410
+ last_exc = gn_exc(
411
+ message=f"{e.__class__.__name__} {e.details()}",
412
+ imetadata=e.initial_metadata(), # type: ignore
413
+ request_id=req_id,
414
+ verbose=verbose,
415
+ )
416
+ last_exc.__cause__ = e
417
+ raise last_exc from None
418
+ else:
419
+ last_exc = None
420
+ break
421
+
422
+ if last_exc is not None:
423
+ raise last_exc
424
+ if response is None:
425
+ raise Exception()
426
+ else:
427
+ return response
428
+
429
+
430
+ def make_req_id() -> str:
431
+ return str(uuid.uuid4())
432
+
433
+
434
+ def get_cert(cert_file: Optional[str]) -> Optional[bytes]:
435
+ cert: Optional[bytes] = None
436
+ if cert_file:
437
+ _logger.debug("open cert_file %s", cert_file)
438
+ with open(cert_file, "rb") as f:
439
+ cert = f.read()
440
+ return cert
441
+
442
+
443
+ def format_long_msg(msg: str, max_len: int) -> str:
444
+ if len(msg) <= max_len:
445
+ return msg
446
+ return "%s... and %s more" % (msg[:max_len], len(msg) - max_len)
447
+
448
+
449
+ def make_cmd(
450
+ hostname: str,
451
+ cmd: str,
452
+ trace: bool = False,
453
+ qa: Optional[List[QA]] = None,
454
+ read_timeout: float = 0.0,
455
+ cmd_timeout: float = 0.0,
456
+ host_params: Optional[HostParams] = None,
457
+ ) -> Message:
458
+ qa_cmd: List[Message] = []
459
+ if qa:
460
+ for item in qa:
461
+ qaitem = server_pb2.QA()
462
+ qaitem.question = item.question
463
+ qaitem.answer = item.answer
464
+ qa_cmd.append(qaitem)
465
+ res = server_pb2.CMD(
466
+ host=hostname,
467
+ cmd=cmd,
468
+ trace=trace,
469
+ qa=qa_cmd,
470
+ read_timeout=read_timeout,
471
+ cmd_timeout=cmd_timeout,
472
+ host_params=host_params.make_pb(),
473
+ )
474
+ return res # type: ignore
475
+
476
+
477
+ def make_files_request(files: Dict[str, File]) -> List[server_pb2.FileData]:
478
+ res: List[server_pb2.FileData] = []
479
+ for path, file in files.items():
480
+ res.append(server_pb2.FileData(path=path, data=file.content))
481
+ return res