flwr-nightly 1.11.0.dev20240815__py3-none-any.whl → 1.11.0.dev20240817__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.

Files changed (32) hide show
  1. flwr/cli/config_utils.py +2 -2
  2. flwr/cli/run/run.py +5 -4
  3. flwr/client/app.py +97 -11
  4. flwr/client/{process → clientapp}/__init__.py +7 -0
  5. flwr/client/clientapp/app.py +178 -0
  6. flwr/client/clientapp/clientappio_servicer.py +238 -0
  7. flwr/client/clientapp/utils.py +108 -0
  8. flwr/client/grpc_rere_client/connection.py +8 -1
  9. flwr/client/rest_client/connection.py +14 -2
  10. flwr/client/supernode/__init__.py +0 -2
  11. flwr/client/supernode/app.py +21 -120
  12. flwr/proto/clientappio_pb2.py +17 -13
  13. flwr/proto/clientappio_pb2.pyi +17 -0
  14. flwr/proto/clientappio_pb2_grpc.py +34 -0
  15. flwr/proto/clientappio_pb2_grpc.pyi +13 -0
  16. flwr/server/app.py +3 -0
  17. flwr/server/run_serverapp.py +18 -2
  18. flwr/server/server.py +3 -1
  19. flwr/server/superlink/driver/driver_servicer.py +23 -8
  20. flwr/server/superlink/ffs/disk_ffs.py +6 -3
  21. flwr/server/superlink/ffs/ffs.py +3 -3
  22. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +9 -3
  23. flwr/server/superlink/fleet/message_handler/message_handler.py +16 -1
  24. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  25. flwr/server/workflow/default_workflows.py +3 -1
  26. flwr/superexec/deployment.py +8 -9
  27. {flwr_nightly-1.11.0.dev20240815.dist-info → flwr_nightly-1.11.0.dev20240817.dist-info}/METADATA +1 -1
  28. {flwr_nightly-1.11.0.dev20240815.dist-info → flwr_nightly-1.11.0.dev20240817.dist-info}/RECORD +31 -29
  29. {flwr_nightly-1.11.0.dev20240815.dist-info → flwr_nightly-1.11.0.dev20240817.dist-info}/entry_points.txt +1 -1
  30. flwr/client/process/clientappio_servicer.py +0 -145
  31. {flwr_nightly-1.11.0.dev20240815.dist-info → flwr_nightly-1.11.0.dev20240817.dist-info}/LICENSE +0 -0
  32. {flwr_nightly-1.11.0.dev20240815.dist-info → flwr_nightly-1.11.0.dev20240817.dist-info}/WHEEL +0 -0
@@ -23,7 +23,13 @@ from uuid import UUID
23
23
  import grpc
24
24
 
25
25
  from flwr.common.logger import log
26
- from flwr.common.serde import user_config_from_proto, user_config_to_proto
26
+ from flwr.common.serde import (
27
+ fab_from_proto,
28
+ fab_to_proto,
29
+ user_config_from_proto,
30
+ user_config_to_proto,
31
+ )
32
+ from flwr.common.typing import Fab
27
33
  from flwr.proto import driver_pb2_grpc # pylint: disable=E0611
28
34
  from flwr.proto.driver_pb2 import ( # pylint: disable=E0611
29
35
  CreateRunRequest,
@@ -43,7 +49,7 @@ from flwr.proto.run_pb2 import ( # pylint: disable=E0611
43
49
  Run,
44
50
  )
45
51
  from flwr.proto.task_pb2 import TaskRes # pylint: disable=E0611
46
- from flwr.server.superlink.ffs import Ffs
52
+ from flwr.server.superlink.ffs.ffs import Ffs
47
53
  from flwr.server.superlink.ffs.ffs_factory import FfsFactory
48
54
  from flwr.server.superlink.state import State, StateFactory
49
55
  from flwr.server.utils.validator import validate_task_ins_or_res
@@ -74,12 +80,13 @@ class DriverServicer(driver_pb2_grpc.DriverServicer):
74
80
  """Create run ID."""
75
81
  log(DEBUG, "DriverServicer.CreateRun")
76
82
  state: State = self.state_factory.state()
77
- if request.HasField("fab") and request.fab.HasField("content"):
83
+ if request.HasField("fab"):
84
+ fab = fab_from_proto(request.fab)
78
85
  ffs: Ffs = self.ffs_factory.ffs()
79
- fab_hash = ffs.put(request.fab.content, {})
86
+ fab_hash = ffs.put(fab.content, {})
80
87
  _raise_if(
81
- fab_hash != request.fab.hash_str,
82
- f"FAB ({request.fab}) hash from request doesn't match contents",
88
+ fab_hash != fab.hash_str,
89
+ f"FAB ({fab.hash_str}) hash from request doesn't match contents",
83
90
  )
84
91
  else:
85
92
  fab_hash = ""
@@ -174,14 +181,22 @@ class DriverServicer(driver_pb2_grpc.DriverServicer):
174
181
  fab_id=run.fab_id,
175
182
  fab_version=run.fab_version,
176
183
  override_config=user_config_to_proto(run.override_config),
184
+ fab_hash=run.fab_hash,
177
185
  )
178
186
  )
179
187
 
180
188
  def GetFab(
181
189
  self, request: GetFabRequest, context: grpc.ServicerContext
182
190
  ) -> GetFabResponse:
183
- """Will be implemented later."""
184
- raise NotImplementedError
191
+ """Get FAB from Ffs."""
192
+ log(DEBUG, "DriverServicer.GetFab")
193
+
194
+ ffs: Ffs = self.ffs_factory.ffs()
195
+ if result := ffs.get(request.hash_str):
196
+ fab = Fab(request.hash_str, result[0])
197
+ return GetFabResponse(fab=fab_to_proto(fab))
198
+
199
+ raise ValueError(f"Found no FAB with hash: {request.hash_str}")
185
200
 
186
201
 
187
202
  def _raise_if(validation_error: bool, detail: str) -> None:
@@ -17,7 +17,7 @@
17
17
  import hashlib
18
18
  import json
19
19
  from pathlib import Path
20
- from typing import Dict, List, Tuple
20
+ from typing import Dict, List, Optional, Tuple
21
21
 
22
22
  from flwr.server.superlink.ffs.ffs import Ffs
23
23
 
@@ -58,7 +58,7 @@ class DiskFfs(Ffs): # pylint: disable=R0904
58
58
 
59
59
  return content_hash
60
60
 
61
- def get(self, key: str) -> Tuple[bytes, Dict[str, str]]:
61
+ def get(self, key: str) -> Optional[Tuple[bytes, Dict[str, str]]]:
62
62
  """Return tuple containing the object content and metadata.
63
63
 
64
64
  Parameters
@@ -68,9 +68,12 @@ class DiskFfs(Ffs): # pylint: disable=R0904
68
68
 
69
69
  Returns
70
70
  -------
71
- Tuple[bytes, Dict[str, str]]
71
+ Optional[Tuple[bytes, Dict[str, str]]]
72
72
  A tuple containing the object content and metadata.
73
73
  """
74
+ if not (self.base_dir / key).exists():
75
+ return None
76
+
74
77
  content = (self.base_dir / key).read_bytes()
75
78
  meta = json.loads((self.base_dir / f"{key}.META").read_text())
76
79
 
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  import abc
19
- from typing import Dict, List, Tuple
19
+ from typing import Dict, List, Optional, Tuple
20
20
 
21
21
 
22
22
  class Ffs(abc.ABC): # pylint: disable=R0904
@@ -40,7 +40,7 @@ class Ffs(abc.ABC): # pylint: disable=R0904
40
40
  """
41
41
 
42
42
  @abc.abstractmethod
43
- def get(self, key: str) -> Tuple[bytes, Dict[str, str]]:
43
+ def get(self, key: str) -> Optional[Tuple[bytes, Dict[str, str]]]:
44
44
  """Return tuple containing the object content and metadata.
45
45
 
46
46
  Parameters
@@ -50,7 +50,7 @@ class Ffs(abc.ABC): # pylint: disable=R0904
50
50
 
51
51
  Returns
52
52
  -------
53
- Tuple[bytes, Dict[str, str]]
53
+ Optional[Tuple[bytes, Dict[str, str]]]
54
54
  A tuple containing the object content and metadata.
55
55
  """
56
56
 
@@ -35,6 +35,7 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
35
35
  PushTaskResResponse,
36
36
  )
37
37
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
38
+ from flwr.server.superlink.ffs.ffs_factory import FfsFactory
38
39
  from flwr.server.superlink.fleet.message_handler import message_handler
39
40
  from flwr.server.superlink.state import StateFactory
40
41
 
@@ -42,8 +43,9 @@ from flwr.server.superlink.state import StateFactory
42
43
  class FleetServicer(fleet_pb2_grpc.FleetServicer):
43
44
  """Fleet API servicer."""
44
45
 
45
- def __init__(self, state_factory: StateFactory) -> None:
46
+ def __init__(self, state_factory: StateFactory, ffs_factory: FfsFactory) -> None:
46
47
  self.state_factory = state_factory
48
+ self.ffs_factory = ffs_factory
47
49
 
48
50
  def CreateNode(
49
51
  self, request: CreateNodeRequest, context: grpc.ServicerContext
@@ -106,5 +108,9 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
106
108
  def GetFab(
107
109
  self, request: GetFabRequest, context: grpc.ServicerContext
108
110
  ) -> GetFabResponse:
109
- """Will be implemented later."""
110
- raise NotImplementedError
111
+ """Get FAB."""
112
+ log(DEBUG, "DriverServicer.GetFab")
113
+ return message_handler.get_fab(
114
+ request=request,
115
+ ffs=self.ffs_factory.ffs(),
116
+ )
@@ -19,7 +19,9 @@ import time
19
19
  from typing import List, Optional
20
20
  from uuid import UUID
21
21
 
22
- from flwr.common.serde import user_config_to_proto
22
+ from flwr.common.serde import fab_to_proto, user_config_to_proto
23
+ from flwr.common.typing import Fab
24
+ from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
23
25
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
24
26
  CreateNodeRequest,
25
27
  CreateNodeResponse,
@@ -40,6 +42,7 @@ from flwr.proto.run_pb2 import ( # pylint: disable=E0611
40
42
  Run,
41
43
  )
42
44
  from flwr.proto.task_pb2 import TaskIns, TaskRes # pylint: disable=E0611
45
+ from flwr.server.superlink.ffs.ffs import Ffs
43
46
  from flwr.server.superlink.state import State
44
47
 
45
48
 
@@ -124,5 +127,17 @@ def get_run(
124
127
  fab_id=run.fab_id,
125
128
  fab_version=run.fab_version,
126
129
  override_config=user_config_to_proto(run.override_config),
130
+ fab_hash=run.fab_hash,
127
131
  )
128
132
  )
133
+
134
+
135
+ def get_fab(
136
+ request: GetFabRequest, ffs: Ffs # pylint: disable=W0613
137
+ ) -> GetFabResponse:
138
+ """Get FAB."""
139
+ if result := ffs.get(request.hash_str):
140
+ fab = Fab(request.hash_str, result[0])
141
+ return GetFabResponse(fab=fab_to_proto(fab))
142
+
143
+ raise ValueError(f"Found no FAB with hash: {request.hash_str}")
@@ -27,8 +27,8 @@ from time import sleep
27
27
  from typing import Callable, Dict, Optional
28
28
 
29
29
  from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError
30
+ from flwr.client.clientapp.utils import get_load_client_app_fn
30
31
  from flwr.client.node_state import NodeState
31
- from flwr.client.supernode.app import _get_load_client_app_fn
32
32
  from flwr.common.constant import (
33
33
  NUM_PARTITIONS_KEY,
34
34
  PARTITION_ID_KEY,
@@ -345,7 +345,7 @@ def start_vce(
345
345
  def _load() -> ClientApp:
346
346
 
347
347
  if client_app_attr:
348
- app = _get_load_client_app_fn(
348
+ app = get_load_client_app_fn(
349
349
  default_app_ref=client_app_attr,
350
350
  app_path=app_dir,
351
351
  flwr_dir=flwr_dir,
@@ -167,7 +167,7 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None:
167
167
  context.state.parameters_records[MAIN_PARAMS_RECORD] = paramsrecord
168
168
 
169
169
  # Evaluate initial parameters
170
- log(INFO, "Evaluating initial global parameters")
170
+ log(INFO, "Starting evaluation of initial global parameters")
171
171
  parameters = compat.parametersrecord_to_parameters(paramsrecord, keep_input=True)
172
172
  res = context.strategy.evaluate(0, parameters=parameters)
173
173
  if res is not None:
@@ -179,6 +179,8 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None:
179
179
  )
180
180
  context.history.add_loss_centralized(server_round=0, loss=res[0])
181
181
  context.history.add_metrics_centralized(server_round=0, metrics=res[1])
182
+ else:
183
+ log(INFO, "Evaluation returned no results (`None`)")
182
184
 
183
185
 
184
186
  def default_centralized_evaluation_workflow(_: Driver, context: Context) -> None:
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  """Deployment engine executor."""
16
16
 
17
+ import hashlib
17
18
  import subprocess
18
19
  from logging import ERROR, INFO
19
20
  from pathlib import Path
@@ -21,12 +22,11 @@ from typing import Optional
21
22
 
22
23
  from typing_extensions import override
23
24
 
24
- from flwr.cli.config_utils import get_fab_metadata
25
25
  from flwr.cli.install import install_from_fab
26
26
  from flwr.common.grpc import create_channel
27
27
  from flwr.common.logger import log
28
- from flwr.common.serde import user_config_to_proto
29
- from flwr.common.typing import UserConfig
28
+ from flwr.common.serde import fab_to_proto, user_config_to_proto
29
+ from flwr.common.typing import Fab, UserConfig
30
30
  from flwr.proto.driver_pb2 import CreateRunRequest # pylint: disable=E0611
31
31
  from flwr.proto.driver_pb2_grpc import DriverStub
32
32
  from flwr.server.driver.grpc_driver import DEFAULT_SERVER_ADDRESS_DRIVER
@@ -113,8 +113,7 @@ class DeploymentEngine(Executor):
113
113
 
114
114
  def _create_run(
115
115
  self,
116
- fab_id: str,
117
- fab_version: str,
116
+ fab: Fab,
118
117
  override_config: UserConfig,
119
118
  ) -> int:
120
119
  if self.stub is None:
@@ -123,8 +122,7 @@ class DeploymentEngine(Executor):
123
122
  assert self.stub is not None
124
123
 
125
124
  req = CreateRunRequest(
126
- fab_id=fab_id,
127
- fab_version=fab_version,
125
+ fab=fab_to_proto(fab),
128
126
  override_config=user_config_to_proto(override_config),
129
127
  )
130
128
  res = self.stub.CreateRun(request=req)
@@ -140,11 +138,12 @@ class DeploymentEngine(Executor):
140
138
  """Start run using the Flower Deployment Engine."""
141
139
  try:
142
140
  # Install FAB to flwr dir
143
- fab_version, fab_id = get_fab_metadata(fab_file)
144
141
  install_from_fab(fab_file, None, True)
145
142
 
146
143
  # Call SuperLink to create run
147
- run_id: int = self._create_run(fab_id, fab_version, override_config)
144
+ run_id: int = self._create_run(
145
+ Fab(hashlib.sha256(fab_file).hexdigest(), fab_file), override_config
146
+ )
148
147
  log(INFO, "Created run %s", str(run_id))
149
148
 
150
149
  command = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.11.0.dev20240815
3
+ Version: 1.11.0.dev20240817
4
4
  Summary: Flower: A Friendly Federated Learning Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -2,7 +2,7 @@ flwr/__init__.py,sha256=VmBWedrCxqmt4QvUHBLqyVEH6p7zaFMD_oCHerXHSVw,937
2
2
  flwr/cli/__init__.py,sha256=cZJVgozlkC6Ni2Hd_FAIrqefrkCGOV18fikToq-6iLw,720
3
3
  flwr/cli/app.py,sha256=FBcSrE35ll88VE11ib67qgsJe2GYDN25UswV9-cYcX8,1267
4
4
  flwr/cli/build.py,sha256=gIR-nTgmLJY5ZtJFLN5ebFBCN3_hoqaioFT77AHojNU,5159
5
- flwr/cli/config_utils.py,sha256=WK6ywT-mHt2iMG90bspkSGMewv8jXh7yQPVdcPuT2JE,7540
5
+ flwr/cli/config_utils.py,sha256=mDGXbcIxG14UpkUplILBYUkSk5M1LeTzZYDGNx-pFpU,7540
6
6
  flwr/cli/example.py,sha256=1bGDYll3BXQY2kRqSN-oICqS5n1b9m0g0RvXTopXHl4,2215
7
7
  flwr/cli/install.py,sha256=AI6Zv2dQVDHpLDX1Z_vX5XHVxmZo1OU3ndCSrD2stzQ,7059
8
8
  flwr/cli/new/__init__.py,sha256=cQzK1WH4JP2awef1t2UQ2xjl1agVEz9rwutV18SWV1k,789
@@ -50,12 +50,16 @@ flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl,sha256=vIO1ArukTC76ogYLNmJ
50
50
  flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl,sha256=jk_5teoyOVM9QdBea8J-nk10S6TKw81QZiiKB54ATF0,654
51
51
  flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl,sha256=bRIvPCPvTTI4Eo5b61Rmw8WdDw3sjcohciTXgULN5l8,702
52
52
  flwr/cli/run/__init__.py,sha256=oCd6HmQDx-sqver1gecgx-uMA38BLTSiiKpl7RGNceg,789
53
- flwr/cli/run/run.py,sha256=Au7v9imSRRETacU16CJScbp5lr-jcduHen_ZFCeO6eY,7672
53
+ flwr/cli/run/run.py,sha256=Ero4hxwZqx5ztFWvaJqslYCUBgp-AmImkomRosdGXyc,7661
54
54
  flwr/cli/utils.py,sha256=l65Ul0YsSBPuypk0uorAtEDmLEYiUrzpCXi6zCg9mJ4,4506
55
55
  flwr/client/__init__.py,sha256=wzJZsYJIHf_8-PMzvfbinyzzjgh1UP1vLrAw2_yEbKI,1345
56
- flwr/client/app.py,sha256=4Phq-PQ56kXRZ_hqUhet72JO28To0lgwYLuwYhCa4Eo,26170
56
+ flwr/client/app.py,sha256=dtdgmm4xVFhkQvymf2vAjbawE9hIFkflzLBxWcYOJjA,30268
57
57
  flwr/client/client.py,sha256=Vp9UkOkoHdNfn6iMYZsj_5m_GICiFfUlKEVaLad-YhM,8183
58
58
  flwr/client/client_app.py,sha256=WcO4r6wrdfaus__3s22D2sYjfcptdgmVujUAYdNE6HU,10393
59
+ flwr/client/clientapp/__init__.py,sha256=kZqChGnTChQ1WGSUkIlW2S5bc0d0mzDubCAmZUGRpEY,800
60
+ flwr/client/clientapp/app.py,sha256=SVzkUAzaHWSH1Sbe2GzLMF-OnrbZ-iKg-QphuEHl8QE,5706
61
+ flwr/client/clientapp/clientappio_servicer.py,sha256=lIH92rHuRQZUaEf9u8fNuiLIQIVYdAp4C1wPok62adQ,8302
62
+ flwr/client/clientapp/utils.py,sha256=2fYKY1LfZPalG5Cm5FbSuNMIDtouQg17GbrzPINyM_A,3990
59
63
  flwr/client/dpfedavg_numpy_client.py,sha256=ylZ-LpBIKmL1HCiS8kq4pkp2QGalc8rYEzDHdRG3VRQ,7435
60
64
  flwr/client/grpc_adapter_client/__init__.py,sha256=QyNWIbsq9DpyMk7oemiO1P3TBFfkfkctnJ1JoAkTl3s,742
61
65
  flwr/client/grpc_adapter_client/connection.py,sha256=aOlCYasl8f2CrfcN-WrVEmvjAZFfEGWFOLwD6K9R0ZQ,3951
@@ -63,7 +67,7 @@ flwr/client/grpc_client/__init__.py,sha256=LsnbqXiJhgQcB0XzAlUQgPx011Uf7Y7yabIC1
63
67
  flwr/client/grpc_client/connection.py,sha256=czhRm23fwTgjN24Vf5nyNQ3hcb5Fo_5k-o9yZnelQCs,9362
64
68
  flwr/client/grpc_rere_client/__init__.py,sha256=MK-oSoV3kwUEQnIwl0GN4OpiHR7eLOrMA8ikunET130,752
65
69
  flwr/client/grpc_rere_client/client_interceptor.py,sha256=sYPEznuQPdy2BPDlvM9FK0ZRRucb4NfwUee1Z_mN82E,4954
66
- flwr/client/grpc_rere_client/connection.py,sha256=-1hVwuVNKo3R5ZcEqKbAQT6I4L6P5fIKsQxHcwf0QNk,10584
70
+ flwr/client/grpc_rere_client/connection.py,sha256=aiIWW9fVgJZNeZ9SjUAx5ax-3825JrjYc5E5l7XvzyM,10913
67
71
  flwr/client/grpc_rere_client/grpc_adapter.py,sha256=Pw7Toi4wCUIEdBMyv4yKirjgW6814gFhhAmsTYmV4IM,5005
68
72
  flwr/client/heartbeat.py,sha256=cx37mJBH8LyoIN4Lks85wtqT1mnU5GulQnr4pGCvAq0,2404
69
73
  flwr/client/message_handler/__init__.py,sha256=QxxQuBNpFPTHx3KiUNvQSlqMKlEnbRR1kFfc1KVje08,719
@@ -80,12 +84,10 @@ flwr/client/mod/utils.py,sha256=UAJXiB0wwVyLkCkpW_i5BXikdBR65p8sNFr7VNHm2nk,1226
80
84
  flwr/client/node_state.py,sha256=Z0ZUPL5BWde9nfAPwR7cSV9YiNQeiEHbuMKDxqpy6-M,3492
81
85
  flwr/client/node_state_tests.py,sha256=-4fVsn7y-z9NYBuhq-cjepgxgVuPqqQgDOL4SofrdIo,2239
82
86
  flwr/client/numpy_client.py,sha256=u76GWAdHmJM88Agm2EgLQSvO8Jnk225mJTk-_TmPjFE,10283
83
- flwr/client/process/__init__.py,sha256=s3hTr2dOWHkSnwivaL7YbXur38bEDgSPKKvwFLa1204,711
84
- flwr/client/process/clientappio_servicer.py,sha256=ygh6wFC_q7fHV5L9G_W3Ftkn5la_BJixtOW-QzwxqyY,5536
85
87
  flwr/client/rest_client/__init__.py,sha256=5KGlp7pjc1dhNRkKlaNtUfQmg8wrRFh9lS3P3uRS-7Q,735
86
- flwr/client/rest_client/connection.py,sha256=YOE2cfgRtkpNveeg88GZTTjUscgGN8XF2RAJ-M_TQ_U,12384
87
- flwr/client/supernode/__init__.py,sha256=8JuFMx5B06SymNWZ9ndvUimD0mopq0Yp2RdQaUANsw8,937
88
- flwr/client/supernode/app.py,sha256=mbSs612GCpRsv6xIi_TD_V2Gi7aYBdpo_e4nKs8kAvI,15910
88
+ flwr/client/rest_client/connection.py,sha256=SglZC4jpqc_0-VBo9cBHa1_2RO9TfPUULQ49DnYeFS0,12767
89
+ flwr/client/supernode/__init__.py,sha256=SUhWOzcgXRNXk1V9UgB5-FaWukqqrOEajVUHEcPkwyQ,865
90
+ flwr/client/supernode/app.py,sha256=CrZG1AM19dgubuGOP0rWYnLwpCkt9F30v137BETSP30,12595
89
91
  flwr/client/typing.py,sha256=dxoTBnTMfqXr5J7G3y-uNjqxYCddvxhu89spfj4Lm2U,1048
90
92
  flwr/common/__init__.py,sha256=4cBLNNnNTwHDnL_HCxhU5ILCSZ6fYh3A_aMBtlvHTVw,3721
91
93
  flwr/common/address.py,sha256=wRu1Luezx1PWadwV9OA_KNko01oVvbRnPqfzaDn8QOk,1882
@@ -125,10 +127,10 @@ flwr/common/telemetry.py,sha256=nSjJHDitPhzB2qUl6LeSMT9Zld5lIk9uW98RpxQwiZw,8366
125
127
  flwr/common/typing.py,sha256=rGabiSkjFvGIHwmhDqtuu-LBvz7LVSj1vyMlNtA7VA0,5004
126
128
  flwr/common/version.py,sha256=W1ntylR04xkCP6zeSet6sRtBn7P1cje2lOqBJgYBjJY,1349
127
129
  flwr/proto/__init__.py,sha256=hbY7JYakwZwCkYgCNlmHdc8rtvfoJbAZLalMdc--CGc,683
128
- flwr/proto/clientappio_pb2.py,sha256=018Lj2HBXmEeq6KsmqFVVx2DGO2nDQqTA6YHEPjCTbc,3242
129
- flwr/proto/clientappio_pb2.pyi,sha256=M0-zbx5cNMUtGY7orsSQWwXo5EbN0CvwQPTuQFwXaCg,4855
130
- flwr/proto/clientappio_pb2_grpc.py,sha256=7WjUVfUUKctIloX3bkKjDprhtkqj2d9Xj3UlYxKzp1s,4513
131
- flwr/proto/clientappio_pb2_grpc.pyi,sha256=30X6jhMIjy5w3iZsDCBGDDwNwj3vj3RslZVWkRvzTn0,1413
130
+ flwr/proto/clientappio_pb2.py,sha256=oGaTBdHuO4pL5oj2zIXG6kOdOdzZ8qaoP6I_oMOEnAY,3636
131
+ flwr/proto/clientappio_pb2.pyi,sha256=7QUU3731Cin-fS-MYwfyU3GDndV_gMwnwOiU0jezHpY,5460
132
+ flwr/proto/clientappio_pb2_grpc.py,sha256=G35GhZ3iEOL8N6tu7Kn_ip4QUx4O2HveXngHAuU2YEM,6112
133
+ flwr/proto/clientappio_pb2_grpc.pyi,sha256=cybktpMPaIMwrItd8hQaQDnRv4zNu_wgRddSqR9REyI,1822
132
134
  flwr/proto/common_pb2.py,sha256=uzSmq0FJdC-MriN9UGPFs7QVIFTKJmX5lyLnzcyZ5WE,2405
133
135
  flwr/proto/common_pb2.pyi,sha256=0ylFO7G79qqLuRg9IQUCBdgyIIFv4m8VzrfoWad4xXU,5394
134
136
  flwr/proto/common_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
@@ -183,7 +185,7 @@ flwr/proto/transport_pb2_grpc.py,sha256=vLN3EHtx2aEEMCO4f1Upu-l27BPzd3-5pV-u8wPc
183
185
  flwr/proto/transport_pb2_grpc.pyi,sha256=AGXf8RiIiW2J5IKMlm_3qT3AzcDa4F3P5IqUjve_esA,766
184
186
  flwr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
187
  flwr/server/__init__.py,sha256=BxzPhvouvWFGi7CFpI5b4EeVR9XDqbK7Ndqg24EL_Rw,1679
186
- flwr/server/app.py,sha256=haNCyIL0v6jCwZemjzok_LB8zWxL_T9J4dOZ7AY2x78,24153
188
+ flwr/server/app.py,sha256=YhGK5LKD12yZOXJC6p5deAodweV91c0IInyiHx7PnWA,24252
187
189
  flwr/server/client_manager.py,sha256=T8UDSRJBVD3fyIDI7NTAA-NA7GPrMNNgH2OAF54RRxE,6127
188
190
  flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2399
189
191
  flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
@@ -197,8 +199,8 @@ flwr/server/driver/driver.py,sha256=NT_yaeit7_kZEIsCEqOWPID1GrVD3ywH4xZ2wtIh5lM,
197
199
  flwr/server/driver/grpc_driver.py,sha256=EbnOtrTR4cmu2ZhmPqKEmOHAhNQD1fd0hAd_xN1MePQ,9749
198
200
  flwr/server/driver/inmemory_driver.py,sha256=RcK94_NtjGZ4aZDIscnU7A3Uv1u8jGx29-xcbjQvZTM,6444
199
201
  flwr/server/history.py,sha256=bBOHKyX1eQONIsUx4EUU-UnAk1i0EbEl8ioyMq_UWQ8,5063
200
- flwr/server/run_serverapp.py,sha256=8HCGSVaNYd2cnd_j-JpVTRH7Cg8_U7ATZUejiMmJe0o,8894
201
- flwr/server/server.py,sha256=wsXsxMZ9SQ0B42nBnUlcV83NJPycgrgg5bFwcQ4BYBE,17821
202
+ flwr/server/run_serverapp.py,sha256=Lb3n-YEsZ86ZzI9vbhLytr4uiVngZ1LgHhnDtYxn42k,9633
203
+ flwr/server/server.py,sha256=PsJeh7ROKMoeLpIVrmbKoQUvdz-iqb54E4HZJanRUmM,17912
202
204
  flwr/server/server_app.py,sha256=1hul76ospG8L_KooK_ewn1sWPNTNYLTtZMeGNOBNruA,6267
203
205
  flwr/server/server_config.py,sha256=CZaHVAsMvGLjpWVcLPkiYxgJN4xfIyAiUrCI3fETKY4,1349
204
206
  flwr/server/serverapp_components.py,sha256=-IV_CitOfrJclJj2jNdbN1Q65PyFmtKtrTIg1hc6WQw,2118
@@ -229,10 +231,10 @@ flwr/server/strategy/strategy.py,sha256=g6VoIFogEviRub6G4QsKdIp6M_Ek6GhBhqcdNx5u
229
231
  flwr/server/superlink/__init__.py,sha256=8tHYCfodUlRD8PCP9fHgvu8cz5N31A2QoRVL0jDJ15E,707
230
232
  flwr/server/superlink/driver/__init__.py,sha256=_JaRW-FdyikHc7souUrnk3mwTGViraEJCeUBY_M_ocs,712
231
233
  flwr/server/superlink/driver/driver_grpc.py,sha256=XZ8veSfKStieUgkRntvOw10F0fmHytw_lr7CDtgzLpY,2055
232
- flwr/server/superlink/driver/driver_servicer.py,sha256=k_3IuteN0zRDz-xsM7RImHn7JODXrcvTRQdobVH7rCw,6581
234
+ flwr/server/superlink/driver/driver_servicer.py,sha256=H8r5zdQC0ksBHPSzdMYhwIT6CXJYWox0AylDCO0aYak,6978
233
235
  flwr/server/superlink/ffs/__init__.py,sha256=FAY-zShcfPmOxosok2QyT6hTNMNctG8cH9s_nIl8jkI,840
234
- flwr/server/superlink/ffs/disk_ffs.py,sha256=ZsBCQVpuScm5QNul_XWlXU65XEaOGbH9ULIls6h7p1A,3213
235
- flwr/server/superlink/ffs/ffs.py,sha256=AVl1AM7ZfVTPOIEQJuMR-aXUUd2foSkRe49NWoPecL8,2384
236
+ flwr/server/superlink/ffs/disk_ffs.py,sha256=wluGIPZfUZdSPcTHG08nGfZtE4A35zEtLs3OFxWFvhY,3315
237
+ flwr/server/superlink/ffs/ffs.py,sha256=vzldg6ulYOFwOhLMk9LrUADaJaM6klP9H8_JL76iu4U,2414
236
238
  flwr/server/superlink/ffs/ffs_factory.py,sha256=N_eMuUZggotdGiDQ5r_Tf21xsu_ob0e3jyM6ag7d3kk,1490
237
239
  flwr/server/superlink/fleet/__init__.py,sha256=76od-HhYjOUoZFLFDFCFnNHI4JLAmaXQEAyp7LWlQpc,711
238
240
  flwr/server/superlink/fleet/grpc_adapter/__init__.py,sha256=spBQQJeYz8zPOBOfyMLv87kqWPASGB73AymcLXdFaYA,742
@@ -243,17 +245,17 @@ flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py,sha256=hh7ykcLMA_ymmD72eWFM
243
245
  flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py,sha256=h3EhqgelegVC4EjOXH5birmAnMoCBJcP7jpHYCnHZPk,4887
244
246
  flwr/server/superlink/fleet/grpc_bidi/grpc_server.py,sha256=ROH-dr7TQZ9nVXjkKOzMhxidJguX2li8--nDnXRi-dU,12095
245
247
  flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=j2hyC342am-_Hgp1g80Y3fGDzfTI6n8QOOn2PyWf4eg,758
246
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=D7OBFZSDBTpA5MiNHtAmELr569gqhjpCHbFlsvqx4Gg,3705
248
+ flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=nqLILs_LV8T-x0BCLmgHy56ml7abjmHyRgc7xTnVbnw,3941
247
249
  flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=v54f7yEX0Xwqh9KRDPzKgJdSBsosus4RzUyzTl-_u5Q,7738
248
250
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=h8oLD7uo5lKICPy0rRdKRjTYe62u8PKkT_fA4xF5JPA,731
249
- flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=KgNah2AaNDiqsTEa653wQqVGUy8hvNAOFzeUfaQR6-Y,3893
251
+ flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=9qDDPwj3txHPo2dNaWQiO3UpGno5Zm9IMhJXnAPZbqg,4439
250
252
  flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=5jbYbAn75sGv-gBwOPDySE0kz96F6dTYLeMrGqNi4lM,735
251
253
  flwr/server/superlink/fleet/rest_rere/rest_api.py,sha256=yoSU-6nCJF9ASHGNpSY69nZbUhPGXkMIKYDgybKQX3c,7672
252
254
  flwr/server/superlink/fleet/vce/__init__.py,sha256=36MHKiefnJeyjwMQzVUK4m06Ojon3WDcwZGQsAcyVhQ,783
253
255
  flwr/server/superlink/fleet/vce/backend/__init__.py,sha256=psQjI1DQHOM5uWVXM27l_wPHjfEkMUTP-_8-lEFH1JA,1466
254
256
  flwr/server/superlink/fleet/vce/backend/backend.py,sha256=KL0eHScWr_YfP2eY3VP8_OOMgZwnRNW7qpu5J-ISpXI,2212
255
257
  flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=6-CjUTv1cFnAb53cZzEYCuVLrVu60tPczTo8meqeqbk,6289
256
- flwr/server/superlink/fleet/vce/vce_api.py,sha256=AeJBEZIC3V54Wg4bzBJa0aO4IXUNiRzRDRv-EKhqtpQ,12750
258
+ flwr/server/superlink/fleet/vce/vce_api.py,sha256=n9SfUaLDgiPVvGVudSC4dZ37vytEDlPn1WA0iUDWJ1I,12750
257
259
  flwr/server/superlink/state/__init__.py,sha256=Gj2OTFLXvA-mAjBvwuKDM3rDrVaQPcIoybSa2uskMTE,1003
258
260
  flwr/server/superlink/state/in_memory_state.py,sha256=XMcT5WvKPOrFOuKcByr5BRFacX4aR2n9bRkABUuPg-M,13206
259
261
  flwr/server/superlink/state/sqlite_state.py,sha256=N8eOLZUveJOzdzL31-hXRYOMqV_-w75S1InyDenaDWU,29420
@@ -266,7 +268,7 @@ flwr/server/utils/tensorboard.py,sha256=l6aMVdtZbbfCX8uwFW-WxH6P171-R-tulMcPhlyk
266
268
  flwr/server/utils/validator.py,sha256=pzyXoOEEPSoYC2UEzened8IKSFRI-kIqqI0QlwRK9jk,5301
267
269
  flwr/server/workflow/__init__.py,sha256=SXY0XkwbkezFBxxrFB5hKUtmtAgnYISBkPouR1V71ss,902
268
270
  flwr/server/workflow/constant.py,sha256=q4DLdR8Krlxuewq2AQjwTL75hphxE5ODNz4AhViHMXk,1082
269
- flwr/server/workflow/default_workflows.py,sha256=_GqFCaxtiq3_UVCvZWgJ200QroGSI9qibeVcT2R71ao,14003
271
+ flwr/server/workflow/default_workflows.py,sha256=q9f1N2NfOrDrR3S3FH1ePNmTMsUUNwv3cdcAejbGSlk,14086
270
272
  flwr/server/workflow/secure_aggregation/__init__.py,sha256=3XlgDOjD_hcukTGl6Bc1B-8M_dPlVSJuTbvXIbiO-Ic,880
271
273
  flwr/server/workflow/secure_aggregation/secagg_workflow.py,sha256=wpAkYPId0nfK6SgpUAtsCni4_MQLd-uqJ81tUKu3xlI,5838
272
274
  flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=LLW4LfPAVdpMhIjJBopHVl-OltYuVSqsESw3PULcrN8,29694
@@ -279,13 +281,13 @@ flwr/simulation/ray_transport/utils.py,sha256=TYdtfg1P9VfTdLMOJlifInGpxWHYs9UfUq
279
281
  flwr/simulation/run_simulation.py,sha256=pKkyrAWzxf05j8AoXh2eYyYeV6PbAXtq1egv2l5JM3I,23447
280
282
  flwr/superexec/__init__.py,sha256=9h94ogLxi6eJ3bUuJYq3E3pApThSabTPiSmPAGlTkHE,800
281
283
  flwr/superexec/app.py,sha256=bmYl8zABnWka9WhRQxX4p1YAI76cYG655dP09ro-V0o,6485
282
- flwr/superexec/deployment.py,sha256=B--96bAvyotQX-c4-umXIe50uCw4HVjD79rXCQtSUH4,6357
284
+ flwr/superexec/deployment.py,sha256=1qhztkcZDjaSbicligbXGqn49gbpN271rTlEVAnNuWw,6283
283
285
  flwr/superexec/exec_grpc.py,sha256=PhqGoZEpTMxSQmUSV8Wgtzb1Za_pHJ-adZqo5RYnDyE,1942
284
286
  flwr/superexec/exec_servicer.py,sha256=jl0aKVjm0PLQABcTL5c3jdSIzb0Z6hpVOtrAn4Ob7ts,2323
285
287
  flwr/superexec/executor.py,sha256=k_adivto6R2U82DADOHNvdtobehBYreRek1gOEBIQnQ,2318
286
288
  flwr/superexec/simulation.py,sha256=lfdClQYSAIMHe43aJ0Pk-kBw_xoV09LsIMfHo2eo-Ck,6775
287
- flwr_nightly-1.11.0.dev20240815.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
288
- flwr_nightly-1.11.0.dev20240815.dist-info/METADATA,sha256=3zvVGv43ZwKbuaVf2LIn5jvzKs0k-30AY2WBWRLrFWA,15690
289
- flwr_nightly-1.11.0.dev20240815.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
290
- flwr_nightly-1.11.0.dev20240815.dist-info/entry_points.txt,sha256=KqQQGNjnr-2IT-bheOkO9yM4Gv5BtjaBgYKRkp-NhVg,388
291
- flwr_nightly-1.11.0.dev20240815.dist-info/RECORD,,
289
+ flwr_nightly-1.11.0.dev20240817.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
290
+ flwr_nightly-1.11.0.dev20240817.dist-info/METADATA,sha256=AVTluXtc1QOLgptn-tnp6VC_4E4-pjGvwHOiVLNjVA4,15690
291
+ flwr_nightly-1.11.0.dev20240817.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
292
+ flwr_nightly-1.11.0.dev20240817.dist-info/entry_points.txt,sha256=3cDQVJEBRCSLzJrVYAgjXpoCjuQ74I3A9NZ61DOHdVo,388
293
+ flwr_nightly-1.11.0.dev20240817.dist-info/RECORD,,
@@ -6,5 +6,5 @@ flower-superexec=flwr.superexec:run_superexec
6
6
  flower-superlink=flwr.server:run_superlink
7
7
  flower-supernode=flwr.client:run_supernode
8
8
  flwr=flwr.cli.app:app
9
- flwr-clientapp=flwr.client.supernode:flwr_clientapp
9
+ flwr-clientapp=flwr.client.clientapp:flwr_clientapp
10
10
 
@@ -1,145 +0,0 @@
1
- # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- # ==============================================================================
15
- """ClientAppIo API servicer."""
16
-
17
-
18
- from dataclasses import dataclass
19
- from logging import DEBUG, ERROR
20
- from typing import Optional
21
-
22
- import grpc
23
-
24
- from flwr.common import Context, Message, typing
25
- from flwr.common.logger import log
26
- from flwr.common.serde import (
27
- clientappstatus_to_proto,
28
- context_from_proto,
29
- context_to_proto,
30
- message_from_proto,
31
- message_to_proto,
32
- run_to_proto,
33
- )
34
- from flwr.common.typing import Run
35
-
36
- # pylint: disable=E0611
37
- from flwr.proto import clientappio_pb2_grpc
38
- from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0401
39
- PullClientAppInputsRequest,
40
- PullClientAppInputsResponse,
41
- PushClientAppOutputsRequest,
42
- PushClientAppOutputsResponse,
43
- )
44
-
45
-
46
- @dataclass
47
- class ClientAppIoInputs:
48
- """Specify the inputs to the ClientApp."""
49
-
50
- message: Message
51
- context: Context
52
- run: Run
53
- token: int
54
-
55
-
56
- @dataclass
57
- class ClientAppIoOutputs:
58
- """Specify the outputs from the ClientApp."""
59
-
60
- message: Message
61
- context: Context
62
-
63
-
64
- # pylint: disable=C0103,W0613,W0201
65
- class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
66
- """ClientAppIo API servicer."""
67
-
68
- def __init__(self) -> None:
69
- self.clientapp_input: Optional[ClientAppIoInputs] = None
70
- self.clientapp_output: Optional[ClientAppIoOutputs] = None
71
-
72
- def PullClientAppInputs(
73
- self, request: PullClientAppInputsRequest, context: grpc.ServicerContext
74
- ) -> PullClientAppInputsResponse:
75
- """Pull Message, Context, and Run."""
76
- log(DEBUG, "ClientAppIo.PullClientAppInputs")
77
- if self.clientapp_input is None:
78
- raise ValueError(
79
- "ClientAppIoInputs not set before calling `PullClientAppInputs`."
80
- )
81
- if request.token != self.clientapp_input.token:
82
- context.abort(
83
- grpc.StatusCode.INVALID_ARGUMENT,
84
- "Mismatch between ClientApp and SuperNode token",
85
- )
86
- return PullClientAppInputsResponse(
87
- message=message_to_proto(self.clientapp_input.message),
88
- context=context_to_proto(self.clientapp_input.context),
89
- run=run_to_proto(self.clientapp_input.run),
90
- )
91
-
92
- def PushClientAppOutputs(
93
- self, request: PushClientAppOutputsRequest, context: grpc.ServicerContext
94
- ) -> PushClientAppOutputsResponse:
95
- """Push Message and Context."""
96
- log(DEBUG, "ClientAppIo.PushClientAppOutputs")
97
- if self.clientapp_output is None:
98
- raise ValueError(
99
- "ClientAppIoOutputs not set before calling `PushClientAppOutputs`."
100
- )
101
- if self.clientapp_input is None:
102
- raise ValueError(
103
- "ClientAppIoInputs not set before calling `PushClientAppOutputs`."
104
- )
105
- if request.token != self.clientapp_input.token:
106
- context.abort(
107
- grpc.StatusCode.INVALID_ARGUMENT,
108
- "Mismatch between ClientApp and SuperNode token",
109
- )
110
- try:
111
- # Update Message and Context
112
- self.clientapp_output.message = message_from_proto(request.message)
113
- self.clientapp_output.context = context_from_proto(request.context)
114
- # Set status
115
- code = typing.ClientAppOutputCode.SUCCESS
116
- status = typing.ClientAppOutputStatus(code=code, message="Success")
117
- proto_status = clientappstatus_to_proto(status=status)
118
- return PushClientAppOutputsResponse(status=proto_status)
119
- except Exception as e: # pylint: disable=broad-exception-caught
120
- log(ERROR, "ClientApp failed to push message to SuperNode, %s", e)
121
- code = typing.ClientAppOutputCode.UNKNOWN_ERROR
122
- status = typing.ClientAppOutputStatus(code=code, message="Push failed")
123
- proto_status = clientappstatus_to_proto(status=status)
124
- return PushClientAppOutputsResponse(status=proto_status)
125
-
126
- def set_inputs(self, clientapp_input: ClientAppIoInputs) -> None:
127
- """Set ClientApp inputs."""
128
- log(DEBUG, "ClientAppIo.SetInputs")
129
- if self.clientapp_input is not None or self.clientapp_output is not None:
130
- raise ValueError(
131
- "ClientAppIoInputs and ClientAppIoOutputs must not be set before "
132
- "calling `set_inputs`."
133
- )
134
- self.clientapp_input = clientapp_input
135
-
136
- def get_outputs(self) -> ClientAppIoOutputs:
137
- """Get ClientApp outputs."""
138
- log(DEBUG, "ClientAppIo.GetOutputs")
139
- if self.clientapp_output is None:
140
- raise ValueError("ClientAppIoOutputs not set before calling `get_outputs`.")
141
- # Set outputs to a local variable and clear self.clientapp_output
142
- output: ClientAppIoOutputs = self.clientapp_output
143
- self.clientapp_input = None
144
- self.clientapp_output = None
145
- return output