flwr-nightly 1.14.0.dev20241203__py3-none-any.whl → 1.14.0.dev20241205__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/ls.py +201 -75
- flwr/cli/run/run.py +81 -14
- flwr/common/constant.py +11 -0
- flwr/common/logger.py +25 -1
- flwr/proto/exec_pb2.py +18 -14
- flwr/proto/exec_pb2.pyi +26 -2
- flwr/proto/exec_pb2_grpc.py +34 -0
- flwr/proto/exec_pb2_grpc.pyi +13 -0
- flwr/server/superlink/driver/serverappio_servicer.py +1 -1
- flwr/superexec/exec_servicer.py +30 -1
- {flwr_nightly-1.14.0.dev20241203.dist-info → flwr_nightly-1.14.0.dev20241205.dist-info}/METADATA +1 -1
- {flwr_nightly-1.14.0.dev20241203.dist-info → flwr_nightly-1.14.0.dev20241205.dist-info}/RECORD +15 -23
- flwr/proto/common_pb2.py +0 -36
- flwr/proto/common_pb2.pyi +0 -121
- flwr/proto/common_pb2_grpc.py +0 -4
- flwr/proto/common_pb2_grpc.pyi +0 -4
- flwr/proto/control_pb2.py +0 -27
- flwr/proto/control_pb2.pyi +0 -7
- flwr/proto/control_pb2_grpc.py +0 -135
- flwr/proto/control_pb2_grpc.pyi +0 -53
- {flwr_nightly-1.14.0.dev20241203.dist-info → flwr_nightly-1.14.0.dev20241205.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.14.0.dev20241203.dist-info → flwr_nightly-1.14.0.dev20241205.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.14.0.dev20241203.dist-info → flwr_nightly-1.14.0.dev20241205.dist-info}/entry_points.txt +0 -0
flwr/cli/ls.py
CHANGED
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
"""Flower command line interface `ls` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
18
20
|
from datetime import datetime, timedelta
|
|
19
21
|
from logging import DEBUG
|
|
20
22
|
from pathlib import Path
|
|
21
|
-
from typing import Annotated, Any, Optional
|
|
23
|
+
from typing import Annotated, Any, Optional, Union
|
|
22
24
|
|
|
23
25
|
import grpc
|
|
24
26
|
import typer
|
|
25
27
|
from rich.console import Console
|
|
26
28
|
from rich.table import Table
|
|
27
29
|
from rich.text import Text
|
|
30
|
+
from typer import Exit
|
|
28
31
|
|
|
29
32
|
from flwr.cli.config_utils import (
|
|
30
33
|
load_and_validate,
|
|
@@ -32,10 +35,10 @@ from flwr.cli.config_utils import (
|
|
|
32
35
|
validate_federation_in_project_config,
|
|
33
36
|
validate_project_config,
|
|
34
37
|
)
|
|
35
|
-
from flwr.common.constant import FAB_CONFIG_FILE, SubStatus
|
|
38
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
|
|
36
39
|
from flwr.common.date import format_timedelta, isoformat8601_utc
|
|
37
40
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
38
|
-
from flwr.common.logger import log
|
|
41
|
+
from flwr.common.logger import log, redirect_output, remove_emojis, restore_output
|
|
39
42
|
from flwr.common.serde import run_from_proto
|
|
40
43
|
from flwr.common.typing import Run
|
|
41
44
|
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
@@ -44,8 +47,10 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
|
44
47
|
)
|
|
45
48
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
46
49
|
|
|
50
|
+
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
def ls( # pylint: disable=too-many-locals, too-many-branches
|
|
49
54
|
app: Annotated[
|
|
50
55
|
Path,
|
|
51
56
|
typer.Argument(help="Path of the Flower project"),
|
|
@@ -68,54 +73,85 @@ def ls(
|
|
|
68
73
|
help="Specific run ID to display",
|
|
69
74
|
),
|
|
70
75
|
] = None,
|
|
76
|
+
output_format: Annotated[
|
|
77
|
+
str,
|
|
78
|
+
typer.Option(
|
|
79
|
+
"--format",
|
|
80
|
+
case_sensitive=False,
|
|
81
|
+
help="Format output using 'default' view or 'json'",
|
|
82
|
+
),
|
|
83
|
+
] = CliOutputFormat.DEFAULT,
|
|
71
84
|
) -> None:
|
|
72
85
|
"""List runs."""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
86
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
87
|
+
captured_output = io.StringIO()
|
|
88
|
+
try:
|
|
89
|
+
if suppress_output:
|
|
90
|
+
redirect_output(captured_output)
|
|
91
|
+
|
|
92
|
+
# Load and validate federation config
|
|
93
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
bold=True,
|
|
95
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
96
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
97
|
+
config = validate_project_config(config, errors, warnings)
|
|
98
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
99
|
+
federation, config
|
|
89
100
|
)
|
|
90
|
-
raise typer.Exit(code=1)
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
102
|
+
if "address" not in federation_config:
|
|
103
|
+
typer.secho(
|
|
104
|
+
"❌ `flwr ls` currently works with Exec API. Ensure that the correct"
|
|
105
|
+
"Exec API address is provided in the `pyproject.toml`.",
|
|
106
|
+
fg=typer.colors.RED,
|
|
107
|
+
bold=True,
|
|
96
108
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
raise typer.Exit(code=1)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
if runs and run_id is not None:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"The options '--runs' and '--run-id' are mutually exclusive."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
channel = _init_channel(app, federation_config)
|
|
118
|
+
stub = ExecStub(channel)
|
|
119
|
+
|
|
120
|
+
# Display information about a specific run ID
|
|
121
|
+
if run_id is not None:
|
|
122
|
+
typer.echo(f"🔍 Displaying information for run ID {run_id}...")
|
|
123
|
+
restore_output()
|
|
124
|
+
_display_one_run(stub, run_id, output_format)
|
|
125
|
+
# By default, list all runs
|
|
126
|
+
else:
|
|
127
|
+
typer.echo("📄 Listing all runs...")
|
|
128
|
+
restore_output()
|
|
129
|
+
_list_runs(stub, output_format)
|
|
130
|
+
|
|
131
|
+
except ValueError as err:
|
|
132
|
+
typer.secho(
|
|
133
|
+
f"❌ {err}",
|
|
134
|
+
fg=typer.colors.RED,
|
|
135
|
+
bold=True,
|
|
136
|
+
)
|
|
137
|
+
raise typer.Exit(code=1) from err
|
|
138
|
+
finally:
|
|
139
|
+
channel.close()
|
|
140
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
141
|
+
if suppress_output:
|
|
142
|
+
restore_output()
|
|
143
|
+
e_message = captured_output.getvalue()
|
|
144
|
+
_print_json_error(e_message, err)
|
|
106
145
|
else:
|
|
107
|
-
typer.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
f"❌ {err}",
|
|
113
|
-
fg=typer.colors.RED,
|
|
114
|
-
bold=True,
|
|
115
|
-
)
|
|
116
|
-
raise typer.Exit(code=1) from err
|
|
146
|
+
typer.secho(
|
|
147
|
+
f"{err}",
|
|
148
|
+
fg=typer.colors.RED,
|
|
149
|
+
bold=True,
|
|
150
|
+
)
|
|
117
151
|
finally:
|
|
118
|
-
|
|
152
|
+
if suppress_output:
|
|
153
|
+
restore_output()
|
|
154
|
+
captured_output.close()
|
|
119
155
|
|
|
120
156
|
|
|
121
157
|
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
@@ -139,23 +175,13 @@ def _init_channel(app: Path, federation_config: dict[str, Any]) -> grpc.Channel:
|
|
|
139
175
|
return channel
|
|
140
176
|
|
|
141
177
|
|
|
142
|
-
def
|
|
143
|
-
"""Format
|
|
144
|
-
table = Table(header_style="bold cyan", show_lines=True)
|
|
178
|
+
def _format_runs(run_dict: dict[int, Run], now_isoformat: str) -> list[_RunListType]:
|
|
179
|
+
"""Format runs to a list."""
|
|
145
180
|
|
|
146
181
|
def _format_datetime(dt: Optional[datetime]) -> str:
|
|
147
182
|
return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
|
|
148
183
|
|
|
149
|
-
|
|
150
|
-
table.add_column(
|
|
151
|
-
Text("Run ID", justify="center"), style="bright_white", overflow="fold"
|
|
152
|
-
)
|
|
153
|
-
table.add_column(Text("FAB", justify="center"), style="dim white")
|
|
154
|
-
table.add_column(Text("Status", justify="center"))
|
|
155
|
-
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
156
|
-
table.add_column(Text("Created At", justify="center"), style="dim white")
|
|
157
|
-
table.add_column(Text("Running At", justify="center"), style="dim white")
|
|
158
|
-
table.add_column(Text("Finished At", justify="center"), style="dim white")
|
|
184
|
+
run_list: list[_RunListType] = []
|
|
159
185
|
|
|
160
186
|
# Add rows
|
|
161
187
|
for run in sorted(
|
|
@@ -167,15 +193,6 @@ def _format_run_table(run_dict: dict[int, Run], now_isoformat: str) -> Table:
|
|
|
167
193
|
else:
|
|
168
194
|
status_text = f"{run.status.status}:{run.status.sub_status}"
|
|
169
195
|
|
|
170
|
-
# Style the status based on its value
|
|
171
|
-
sub_status = run.status.sub_status
|
|
172
|
-
if sub_status == SubStatus.COMPLETED:
|
|
173
|
-
status_style = "green"
|
|
174
|
-
elif sub_status == SubStatus.FAILED:
|
|
175
|
-
status_style = "red"
|
|
176
|
-
else:
|
|
177
|
-
status_style = "yellow"
|
|
178
|
-
|
|
179
196
|
# Convert isoformat to datetime
|
|
180
197
|
pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
|
|
181
198
|
running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
|
|
@@ -192,31 +209,124 @@ def _format_run_table(run_dict: dict[int, Run], now_isoformat: str) -> Table:
|
|
|
192
209
|
end_time = datetime.fromisoformat(now_isoformat)
|
|
193
210
|
elapsed_time = end_time - running_at
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
run_list.append(
|
|
213
|
+
(
|
|
214
|
+
run.run_id,
|
|
215
|
+
run.fab_id,
|
|
216
|
+
run.fab_version,
|
|
217
|
+
run.fab_hash,
|
|
218
|
+
status_text,
|
|
219
|
+
format_timedelta(elapsed_time),
|
|
220
|
+
_format_datetime(pending_at),
|
|
221
|
+
_format_datetime(running_at),
|
|
222
|
+
_format_datetime(finished_at),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
return run_list
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _to_table(run_list: list[_RunListType]) -> Table:
|
|
229
|
+
"""Format the provided run list to a rich Table."""
|
|
230
|
+
table = Table(header_style="bold cyan", show_lines=True)
|
|
231
|
+
|
|
232
|
+
# Add columns
|
|
233
|
+
table.add_column(
|
|
234
|
+
Text("Run ID", justify="center"), style="bright_white", overflow="fold"
|
|
235
|
+
)
|
|
236
|
+
table.add_column(Text("FAB", justify="center"), style="dim white")
|
|
237
|
+
table.add_column(Text("Status", justify="center"))
|
|
238
|
+
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
239
|
+
table.add_column(Text("Created At", justify="center"), style="dim white")
|
|
240
|
+
table.add_column(Text("Running At", justify="center"), style="dim white")
|
|
241
|
+
table.add_column(Text("Finished At", justify="center"), style="dim white")
|
|
242
|
+
|
|
243
|
+
for row in run_list:
|
|
244
|
+
(
|
|
245
|
+
run_id,
|
|
246
|
+
fab_id,
|
|
247
|
+
fab_version,
|
|
248
|
+
_,
|
|
249
|
+
status_text,
|
|
250
|
+
elapsed,
|
|
251
|
+
created_at,
|
|
252
|
+
running_at,
|
|
253
|
+
finished_at,
|
|
254
|
+
) = row
|
|
255
|
+
# Style the status based on its value
|
|
256
|
+
sub_status = status_text.rsplit(":", maxsplit=1)[-1]
|
|
257
|
+
if sub_status == SubStatus.COMPLETED:
|
|
258
|
+
status_style = "green"
|
|
259
|
+
elif sub_status == SubStatus.FAILED:
|
|
260
|
+
status_style = "red"
|
|
261
|
+
else:
|
|
262
|
+
status_style = "yellow"
|
|
263
|
+
|
|
264
|
+
formatted_row = (
|
|
265
|
+
f"[bold]{run_id}[/bold]",
|
|
266
|
+
f"{fab_id} (v{fab_version})",
|
|
198
267
|
f"[{status_style}]{status_text}[/{status_style}]",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
268
|
+
elapsed,
|
|
269
|
+
created_at,
|
|
270
|
+
running_at,
|
|
271
|
+
finished_at,
|
|
203
272
|
)
|
|
273
|
+
table.add_row(*formatted_row)
|
|
274
|
+
|
|
204
275
|
return table
|
|
205
276
|
|
|
206
277
|
|
|
278
|
+
def _to_json(run_list: list[_RunListType]) -> str:
|
|
279
|
+
"""Format run status list to a JSON formatted string."""
|
|
280
|
+
runs_list = []
|
|
281
|
+
for row in run_list:
|
|
282
|
+
(
|
|
283
|
+
run_id,
|
|
284
|
+
fab_id,
|
|
285
|
+
fab_version,
|
|
286
|
+
fab_hash,
|
|
287
|
+
status_text,
|
|
288
|
+
elapsed,
|
|
289
|
+
created_at,
|
|
290
|
+
running_at,
|
|
291
|
+
finished_at,
|
|
292
|
+
) = row
|
|
293
|
+
runs_list.append(
|
|
294
|
+
{
|
|
295
|
+
"run-id": run_id,
|
|
296
|
+
"fab-id": fab_id,
|
|
297
|
+
"fab-name": fab_id.split("/")[-1],
|
|
298
|
+
"fab-version": fab_version,
|
|
299
|
+
"fab-hash": fab_hash[:8],
|
|
300
|
+
"status": status_text,
|
|
301
|
+
"elapsed": elapsed,
|
|
302
|
+
"created-at": created_at,
|
|
303
|
+
"running-at": running_at,
|
|
304
|
+
"finished-at": finished_at,
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return json.dumps({"success": True, "runs": runs_list})
|
|
309
|
+
|
|
310
|
+
|
|
207
311
|
def _list_runs(
|
|
208
312
|
stub: ExecStub,
|
|
313
|
+
output_format: str = CliOutputFormat.DEFAULT,
|
|
209
314
|
) -> None:
|
|
210
315
|
"""List all runs."""
|
|
211
316
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
|
212
317
|
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
213
318
|
|
|
214
|
-
|
|
319
|
+
formatted_runs = _format_runs(run_dict, res.now)
|
|
320
|
+
if output_format == CliOutputFormat.JSON:
|
|
321
|
+
Console().print_json(_to_json(formatted_runs))
|
|
322
|
+
else:
|
|
323
|
+
Console().print(_to_table(formatted_runs))
|
|
215
324
|
|
|
216
325
|
|
|
217
326
|
def _display_one_run(
|
|
218
327
|
stub: ExecStub,
|
|
219
328
|
run_id: int,
|
|
329
|
+
output_format: str = CliOutputFormat.DEFAULT,
|
|
220
330
|
) -> None:
|
|
221
331
|
"""Display information about a specific run."""
|
|
222
332
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
|
@@ -225,4 +335,20 @@ def _display_one_run(
|
|
|
225
335
|
|
|
226
336
|
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
227
337
|
|
|
228
|
-
|
|
338
|
+
formatted_runs = _format_runs(run_dict, res.now)
|
|
339
|
+
if output_format == CliOutputFormat.JSON:
|
|
340
|
+
Console().print_json(_to_json(formatted_runs))
|
|
341
|
+
else:
|
|
342
|
+
Console().print(_to_table(formatted_runs))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _print_json_error(msg: str, e: Union[Exit, Exception]) -> None:
|
|
346
|
+
"""Print error message as JSON."""
|
|
347
|
+
Console().print_json(
|
|
348
|
+
json.dumps(
|
|
349
|
+
{
|
|
350
|
+
"success": False,
|
|
351
|
+
"error-message": remove_emojis(str(msg) + "\n" + str(e)),
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
)
|
flwr/cli/run/run.py
CHANGED
|
@@ -14,16 +14,19 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `run` command."""
|
|
16
16
|
|
|
17
|
+
import io
|
|
17
18
|
import json
|
|
18
19
|
import subprocess
|
|
19
20
|
from logging import DEBUG
|
|
20
21
|
from pathlib import Path
|
|
21
|
-
from typing import Annotated, Any, Optional
|
|
22
|
+
from typing import Annotated, Any, Optional, Union
|
|
22
23
|
|
|
23
24
|
import typer
|
|
25
|
+
from rich.console import Console
|
|
24
26
|
|
|
25
27
|
from flwr.cli.build import build
|
|
26
28
|
from flwr.cli.config_utils import (
|
|
29
|
+
get_fab_metadata,
|
|
27
30
|
load_and_validate,
|
|
28
31
|
validate_certificate_in_federation_config,
|
|
29
32
|
validate_federation_in_project_config,
|
|
@@ -34,8 +37,9 @@ from flwr.common.config import (
|
|
|
34
37
|
parse_config_args,
|
|
35
38
|
user_config_to_configsrecord,
|
|
36
39
|
)
|
|
40
|
+
from flwr.common.constant import CliOutputFormat
|
|
37
41
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
38
|
-
from flwr.common.logger import log
|
|
42
|
+
from flwr.common.logger import log, redirect_output, remove_emojis, restore_output
|
|
39
43
|
from flwr.common.serde import (
|
|
40
44
|
configs_record_to_proto,
|
|
41
45
|
fab_to_proto,
|
|
@@ -85,21 +89,51 @@ def run(
|
|
|
85
89
|
"logs are not streamed by default.",
|
|
86
90
|
),
|
|
87
91
|
] = False,
|
|
92
|
+
output_format: Annotated[
|
|
93
|
+
str,
|
|
94
|
+
typer.Option(
|
|
95
|
+
"--format",
|
|
96
|
+
case_sensitive=False,
|
|
97
|
+
help="Format output using 'default' view or 'json'",
|
|
98
|
+
),
|
|
99
|
+
] = CliOutputFormat.DEFAULT,
|
|
88
100
|
) -> None:
|
|
89
101
|
"""Run Flower App."""
|
|
90
|
-
|
|
102
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
103
|
+
captured_output = io.StringIO()
|
|
104
|
+
try:
|
|
105
|
+
if suppress_output:
|
|
106
|
+
redirect_output(captured_output)
|
|
107
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
pyproject_path = app / "pyproject.toml" if app else None
|
|
110
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
111
|
+
config = validate_project_config(config, errors, warnings)
|
|
112
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
113
|
+
federation, config
|
|
114
|
+
)
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
if "address" in federation_config:
|
|
117
|
+
_run_with_exec_api(
|
|
118
|
+
app, federation_config, config_overrides, stream, output_format
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
_run_without_exec_api(app, federation_config, config_overrides, federation)
|
|
122
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
123
|
+
if suppress_output:
|
|
124
|
+
restore_output()
|
|
125
|
+
e_message = captured_output.getvalue()
|
|
126
|
+
_print_json_error(e_message, err)
|
|
127
|
+
else:
|
|
128
|
+
typer.secho(
|
|
129
|
+
f"{err}",
|
|
130
|
+
fg=typer.colors.RED,
|
|
131
|
+
bold=True,
|
|
132
|
+
)
|
|
133
|
+
finally:
|
|
134
|
+
if suppress_output:
|
|
135
|
+
restore_output()
|
|
136
|
+
captured_output.close()
|
|
103
137
|
|
|
104
138
|
|
|
105
139
|
# pylint: disable-next=too-many-locals
|
|
@@ -108,6 +142,7 @@ def _run_with_exec_api(
|
|
|
108
142
|
federation_config: dict[str, Any],
|
|
109
143
|
config_overrides: Optional[list[str]],
|
|
110
144
|
stream: bool,
|
|
145
|
+
output_format: str,
|
|
111
146
|
) -> None:
|
|
112
147
|
|
|
113
148
|
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
@@ -125,6 +160,7 @@ def _run_with_exec_api(
|
|
|
125
160
|
|
|
126
161
|
fab_path, fab_hash = build(app)
|
|
127
162
|
content = Path(fab_path).read_bytes()
|
|
163
|
+
fab_id, fab_version = get_fab_metadata(Path(fab_path))
|
|
128
164
|
|
|
129
165
|
# Delete FAB file once the bytes is computed
|
|
130
166
|
Path(fab_path).unlink()
|
|
@@ -142,7 +178,26 @@ def _run_with_exec_api(
|
|
|
142
178
|
)
|
|
143
179
|
res = stub.StartRun(req)
|
|
144
180
|
|
|
145
|
-
|
|
181
|
+
if res.HasField("run_id"):
|
|
182
|
+
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
|
183
|
+
else:
|
|
184
|
+
typer.secho("❌ Failed to start run", fg=typer.colors.RED)
|
|
185
|
+
raise typer.Exit(code=1)
|
|
186
|
+
|
|
187
|
+
if output_format == CliOutputFormat.JSON:
|
|
188
|
+
run_output = json.dumps(
|
|
189
|
+
{
|
|
190
|
+
"success": res.HasField("run_id"),
|
|
191
|
+
"run-id": res.run_id if res.HasField("run_id") else None,
|
|
192
|
+
"fab-id": fab_id,
|
|
193
|
+
"fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
|
|
194
|
+
"fab-version": fab_version,
|
|
195
|
+
"fab-hash": fab_hash[:8],
|
|
196
|
+
"fab-filename": fab_path,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
restore_output()
|
|
200
|
+
Console().print_json(run_output)
|
|
146
201
|
|
|
147
202
|
if stream:
|
|
148
203
|
start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
|
|
@@ -194,3 +249,15 @@ def _run_without_exec_api(
|
|
|
194
249
|
check=True,
|
|
195
250
|
text=True,
|
|
196
251
|
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _print_json_error(msg: str, e: Union[typer.Exit, Exception]) -> None:
|
|
255
|
+
"""Print error message as JSON."""
|
|
256
|
+
Console().print_json(
|
|
257
|
+
json.dumps(
|
|
258
|
+
{
|
|
259
|
+
"success": False,
|
|
260
|
+
"error-message": remove_emojis(str(msg) + "\n" + str(e)),
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
)
|
flwr/common/constant.py
CHANGED
|
@@ -181,3 +181,14 @@ class SubStatus:
|
|
|
181
181
|
def __new__(cls) -> SubStatus:
|
|
182
182
|
"""Prevent instantiation."""
|
|
183
183
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class CliOutputFormat:
|
|
187
|
+
"""Define output format for `flwr` CLI commands."""
|
|
188
|
+
|
|
189
|
+
DEFAULT = "default"
|
|
190
|
+
JSON = "json"
|
|
191
|
+
|
|
192
|
+
def __new__(cls) -> CliOutputFormat:
|
|
193
|
+
"""Prevent instantiation."""
|
|
194
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
flwr/common/logger.py
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower Logger."""
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
import logging
|
|
18
|
+
import re
|
|
19
19
|
import sys
|
|
20
20
|
import threading
|
|
21
21
|
import time
|
|
22
|
+
from io import StringIO
|
|
22
23
|
from logging import WARN, LogRecord
|
|
23
24
|
from logging.handlers import HTTPHandler
|
|
24
25
|
from queue import Empty, Queue
|
|
@@ -303,6 +304,13 @@ def restore_output() -> None:
|
|
|
303
304
|
console_handler.stream = sys.stdout
|
|
304
305
|
|
|
305
306
|
|
|
307
|
+
def redirect_output(output_buffer: StringIO) -> None:
|
|
308
|
+
"""Redirect stdout and stderr to text I/O buffer."""
|
|
309
|
+
sys.stdout = output_buffer
|
|
310
|
+
sys.stderr = output_buffer
|
|
311
|
+
console_handler.stream = sys.stdout
|
|
312
|
+
|
|
313
|
+
|
|
306
314
|
def _log_uploader(
|
|
307
315
|
log_queue: Queue[Optional[str]], node_id: int, run_id: int, stub: ServerAppIoStub
|
|
308
316
|
) -> None:
|
|
@@ -366,3 +374,19 @@ def stop_log_uploader(
|
|
|
366
374
|
"""Stop the log uploader thread."""
|
|
367
375
|
log_queue.put(None)
|
|
368
376
|
log_uploader.join()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def remove_emojis(text: str) -> str:
|
|
380
|
+
"""Remove emojis from the provided text."""
|
|
381
|
+
emoji_pattern = re.compile(
|
|
382
|
+
"["
|
|
383
|
+
"\U0001F600-\U0001F64F" # Emoticons
|
|
384
|
+
"\U0001F300-\U0001F5FF" # Symbols & Pictographs
|
|
385
|
+
"\U0001F680-\U0001F6FF" # Transport & Map Symbols
|
|
386
|
+
"\U0001F1E0-\U0001F1FF" # Flags
|
|
387
|
+
"\U00002702-\U000027B0" # Dingbats
|
|
388
|
+
"\U000024C2-\U0001F251"
|
|
389
|
+
"]+",
|
|
390
|
+
flags=re.UNICODE,
|
|
391
|
+
)
|
|
392
|
+
return emoji_pattern.sub(r"", text)
|
flwr/proto/exec_pb2.py
CHANGED
|
@@ -18,7 +18,7 @@ from flwr.proto import recordset_pb2 as flwr_dot_proto_dot_recordset__pb2
|
|
|
18
18
|
from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x14\x66lwr/proto/run.proto\"\xfb\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x35\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x19.flwr.proto.ConfigsRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\
|
|
21
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x14\x66lwr/proto/run.proto\"\xfb\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x35\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x19.flwr.proto.ConfigsRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x32\xaf\x02\n\x04\x45xec\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x62\x06proto3')
|
|
22
22
|
|
|
23
23
|
_globals = globals()
|
|
24
24
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
@@ -34,17 +34,21 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
|
34
34
|
_globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_start=316
|
|
35
35
|
_globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_end=389
|
|
36
36
|
_globals['_STARTRUNRESPONSE']._serialized_start=391
|
|
37
|
-
_globals['_STARTRUNRESPONSE']._serialized_end=
|
|
38
|
-
_globals['_STREAMLOGSREQUEST']._serialized_start=
|
|
39
|
-
_globals['_STREAMLOGSREQUEST']._serialized_end=
|
|
40
|
-
_globals['_STREAMLOGSRESPONSE']._serialized_start=
|
|
41
|
-
_globals['_STREAMLOGSRESPONSE']._serialized_end=
|
|
42
|
-
_globals['_LISTRUNSREQUEST']._serialized_start=
|
|
43
|
-
_globals['_LISTRUNSREQUEST']._serialized_end=
|
|
44
|
-
_globals['_LISTRUNSRESPONSE']._serialized_start=
|
|
45
|
-
_globals['_LISTRUNSRESPONSE']._serialized_end=
|
|
46
|
-
_globals['_LISTRUNSRESPONSE_RUNDICTENTRY']._serialized_start=
|
|
47
|
-
_globals['_LISTRUNSRESPONSE_RUNDICTENTRY']._serialized_end=
|
|
48
|
-
_globals['
|
|
49
|
-
_globals['
|
|
37
|
+
_globals['_STARTRUNRESPONSE']._serialized_end=441
|
|
38
|
+
_globals['_STREAMLOGSREQUEST']._serialized_start=443
|
|
39
|
+
_globals['_STREAMLOGSREQUEST']._serialized_end=503
|
|
40
|
+
_globals['_STREAMLOGSRESPONSE']._serialized_start=505
|
|
41
|
+
_globals['_STREAMLOGSRESPONSE']._serialized_end=571
|
|
42
|
+
_globals['_LISTRUNSREQUEST']._serialized_start=573
|
|
43
|
+
_globals['_LISTRUNSREQUEST']._serialized_end=622
|
|
44
|
+
_globals['_LISTRUNSRESPONSE']._serialized_start=625
|
|
45
|
+
_globals['_LISTRUNSRESPONSE']._serialized_end=782
|
|
46
|
+
_globals['_LISTRUNSRESPONSE_RUNDICTENTRY']._serialized_start=719
|
|
47
|
+
_globals['_LISTRUNSRESPONSE_RUNDICTENTRY']._serialized_end=782
|
|
48
|
+
_globals['_STOPRUNREQUEST']._serialized_start=784
|
|
49
|
+
_globals['_STOPRUNREQUEST']._serialized_end=816
|
|
50
|
+
_globals['_STOPRUNRESPONSE']._serialized_start=818
|
|
51
|
+
_globals['_STOPRUNRESPONSE']._serialized_end=852
|
|
52
|
+
_globals['_EXEC']._serialized_start=855
|
|
53
|
+
_globals['_EXEC']._serialized_end=1158
|
|
50
54
|
# @@protoc_insertion_point(module_scope)
|
flwr/proto/exec_pb2.pyi
CHANGED
|
@@ -57,9 +57,11 @@ class StartRunResponse(google.protobuf.message.Message):
|
|
|
57
57
|
run_id: builtins.int
|
|
58
58
|
def __init__(self,
|
|
59
59
|
*,
|
|
60
|
-
run_id: builtins.int = ...,
|
|
60
|
+
run_id: typing.Optional[builtins.int] = ...,
|
|
61
61
|
) -> None: ...
|
|
62
|
-
def
|
|
62
|
+
def HasField(self, field_name: typing_extensions.Literal["_run_id",b"_run_id","run_id",b"run_id"]) -> builtins.bool: ...
|
|
63
|
+
def ClearField(self, field_name: typing_extensions.Literal["_run_id",b"_run_id","run_id",b"run_id"]) -> None: ...
|
|
64
|
+
def WhichOneof(self, oneof_group: typing_extensions.Literal["_run_id",b"_run_id"]) -> typing.Optional[typing_extensions.Literal["run_id"]]: ...
|
|
63
65
|
global___StartRunResponse = StartRunResponse
|
|
64
66
|
|
|
65
67
|
class StreamLogsRequest(google.protobuf.message.Message):
|
|
@@ -132,3 +134,25 @@ class ListRunsResponse(google.protobuf.message.Message):
|
|
|
132
134
|
) -> None: ...
|
|
133
135
|
def ClearField(self, field_name: typing_extensions.Literal["now",b"now","run_dict",b"run_dict"]) -> None: ...
|
|
134
136
|
global___ListRunsResponse = ListRunsResponse
|
|
137
|
+
|
|
138
|
+
class StopRunRequest(google.protobuf.message.Message):
|
|
139
|
+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
|
140
|
+
RUN_ID_FIELD_NUMBER: builtins.int
|
|
141
|
+
run_id: builtins.int
|
|
142
|
+
def __init__(self,
|
|
143
|
+
*,
|
|
144
|
+
run_id: builtins.int = ...,
|
|
145
|
+
) -> None: ...
|
|
146
|
+
def ClearField(self, field_name: typing_extensions.Literal["run_id",b"run_id"]) -> None: ...
|
|
147
|
+
global___StopRunRequest = StopRunRequest
|
|
148
|
+
|
|
149
|
+
class StopRunResponse(google.protobuf.message.Message):
|
|
150
|
+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
|
151
|
+
SUCCESS_FIELD_NUMBER: builtins.int
|
|
152
|
+
success: builtins.bool
|
|
153
|
+
def __init__(self,
|
|
154
|
+
*,
|
|
155
|
+
success: builtins.bool = ...,
|
|
156
|
+
) -> None: ...
|
|
157
|
+
def ClearField(self, field_name: typing_extensions.Literal["success",b"success"]) -> None: ...
|
|
158
|
+
global___StopRunResponse = StopRunResponse
|