flwr-nightly 1.9.0.dev20240417__py3-none-any.whl → 1.9.0.dev20240419__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/config_utils.py CHANGED
@@ -15,13 +15,46 @@
15
15
  """Utility to validate the `pyproject.toml` file."""
16
16
 
17
17
  import os
18
+ from pathlib import Path
18
19
  from typing import Any, Dict, List, Optional, Tuple
19
20
 
20
21
  import tomli
22
+ import typer
21
23
 
22
24
  from flwr.common import object_ref
23
25
 
24
26
 
27
+ def validate_project_dir(project_dir: Path) -> Optional[Dict[str, Any]]:
28
+ """Check if a Flower App directory is valid."""
29
+ config = load(str(project_dir / "pyproject.toml"))
30
+ if config is None:
31
+ typer.secho(
32
+ "❌ Project configuration could not be loaded. "
33
+ "`pyproject.toml` does not exist.",
34
+ fg=typer.colors.RED,
35
+ bold=True,
36
+ )
37
+ return None
38
+
39
+ if not validate(config):
40
+ typer.secho(
41
+ "❌ Project configuration is invalid.",
42
+ fg=typer.colors.RED,
43
+ bold=True,
44
+ )
45
+ return None
46
+
47
+ if "publisher" not in config["flower"]:
48
+ typer.secho(
49
+ "❌ Project configuration is missing required `publisher` field.",
50
+ fg=typer.colors.RED,
51
+ bold=True,
52
+ )
53
+ return None
54
+
55
+ return config
56
+
57
+
25
58
  def load_and_validate_with_defaults(
26
59
  path: Optional[str] = None,
27
60
  ) -> Tuple[Optional[Dict[str, Any]], List[str], List[str]]:
flwr/cli/new/new.py CHANGED
@@ -58,8 +58,9 @@ def render_template(template: str, data: Dict[str, str]) -> str:
58
58
  """Render template."""
59
59
  tpl_file = load_template(template)
60
60
  tpl = Template(tpl_file)
61
- result = tpl.substitute(data)
62
- return result
61
+ if ".gitignore" not in template:
62
+ return tpl.substitute(data)
63
+ return tpl.template
63
64
 
64
65
 
65
66
  def create_file(file_path: str, content: str) -> None:
@@ -127,6 +128,7 @@ def new(
127
128
 
128
129
  # List of files to render
129
130
  files = {
131
+ ".gitignore": {"template": "app/.gitignore.tpl"},
130
132
  "README.md": {"template": "app/README.md.tpl"},
131
133
  "pyproject.toml": {"template": f"app/pyproject.{framework_str}.toml.tpl"},
132
134
  f"{pnl}/__init__.py": {"template": "app/code/__init__.py.tpl"},
@@ -134,8 +136,11 @@ def new(
134
136
  f"{pnl}/client.py": {"template": f"app/code/client.{framework_str}.py.tpl"},
135
137
  }
136
138
 
137
- # In case framework is MlFramework.PYTORCH generate additionally the task.py file
138
- if framework_str == MlFramework.PYTORCH.value.lower():
139
+ # Depending on the framework, generate task.py file
140
+ frameworks_with_tasks = [
141
+ MlFramework.PYTORCH.value.lower(),
142
+ ]
143
+ if framework_str in frameworks_with_tasks:
139
144
  files[f"{pnl}/task.py"] = {"template": f"app/code/task.{framework_str}.py.tpl"}
140
145
 
141
146
  context = {"project_name": project_name}
@@ -0,0 +1,160 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
@@ -1 +1,57 @@
1
1
  """$project_name: A Flower / TensorFlow app."""
2
+
3
+ import os
4
+
5
+ import tensorflow as tf
6
+ from flwr.client import ClientApp, NumPyClient
7
+ from flwr_datasets import FederatedDataset
8
+
9
+
10
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
11
+
12
+ # Define Flower client
13
+ class FlowerClient(NumPyClient):
14
+ def __init__(self, model, train_data, test_data):
15
+ self.model = model
16
+ self.x_train, self.y_train = train_data
17
+ self.x_test, self.y_test = test_data
18
+
19
+ def get_parameters(self, config):
20
+ return self.model.get_weights()
21
+
22
+ def fit(self, parameters, config):
23
+ self.model.set_weights(parameters)
24
+ self.model.fit(self.x_train, self.y_train, epochs=1, batch_size=32, verbose=0)
25
+ return self.model.get_weights(), len(self.x_train), {}
26
+
27
+ def evaluate(self, parameters, config):
28
+ self.model.set_weights(parameters)
29
+ loss, accuracy = self.model.evaluate(self.x_test, self.y_test, verbose=0)
30
+ return loss, len(self.x_test), {"accuracy": accuracy}
31
+
32
+
33
+ fds = FederatedDataset(dataset="cifar10", partitioners={"train": 2})
34
+
35
+ def client_fn(cid: str):
36
+ """Create and return an instance of Flower `Client`."""
37
+
38
+ # Load model and data (MobileNetV2, CIFAR-10)
39
+ model = tf.keras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None)
40
+ model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"])
41
+
42
+ # Download and partition dataset
43
+ partition = fds.load_partition(int(cid), "train")
44
+ partition.set_format("numpy")
45
+
46
+ # Divide data on each node: 80% train, 20% test
47
+ partition = partition.train_test_split(test_size=0.2, seed=42)
48
+ train_data = partition["train"]["img"] / 255.0, partition["train"]["label"]
49
+ test_data = partition["test"]["img"] / 255.0, partition["test"]["label"]
50
+
51
+ return FlowerClient(model, train_data, test_data).to_client()
52
+
53
+
54
+ # Flower ClientApp
55
+ app = ClientApp(
56
+ client_fn=client_fn,
57
+ )
@@ -1 +1,19 @@
1
1
  """$project_name: A Flower / TensorFlow app."""
2
+
3
+ from flwr.server import ServerApp, ServerConfig
4
+ from flwr.server.strategy import FedAvg
5
+
6
+ # Define config
7
+ config = ServerConfig(num_rounds=3)
8
+
9
+ strategy = FedAvg(
10
+ fraction_fit=1.0,
11
+ fraction_evaluate=1.0,
12
+ min_available_clients=2,
13
+ )
14
+
15
+ # Flower ServerApp
16
+ app = ServerApp(
17
+ config=config,
18
+ strategy=strategy,
19
+ )
flwr/client/__init__.py CHANGED
@@ -21,6 +21,7 @@ from .app import start_numpy_client as start_numpy_client
21
21
  from .client import Client as Client
22
22
  from .client_app import ClientApp as ClientApp
23
23
  from .numpy_client import NumPyClient as NumPyClient
24
+ from .supernode import run_supernode as run_supernode
24
25
  from .typing import ClientFn as ClientFn
25
26
 
26
27
  __all__ = [
@@ -29,6 +30,7 @@ __all__ = [
29
30
  "ClientFn",
30
31
  "NumPyClient",
31
32
  "run_client_app",
33
+ "run_supernode",
32
34
  "start_client",
33
35
  "start_numpy_client",
34
36
  ]
flwr/client/app.py CHANGED
@@ -47,14 +47,15 @@ from .grpc_rere_client.connection import grpc_request_response
47
47
  from .message_handler.message_handler import handle_control_message
48
48
  from .node_state import NodeState
49
49
  from .numpy_client import NumPyClient
50
+ from .supernode.app import parse_args_run_client_app
50
51
 
51
52
 
52
53
  def run_client_app() -> None:
53
54
  """Run Flower client app."""
54
- event(EventType.RUN_CLIENT_APP_ENTER)
55
-
56
55
  log(INFO, "Long-running Flower client starting")
57
56
 
57
+ event(EventType.RUN_CLIENT_APP_ENTER)
58
+
58
59
  args = _parse_args_run_client_app().parse_args()
59
60
 
60
61
  # Obtain certificates
@@ -131,56 +132,7 @@ def _parse_args_run_client_app() -> argparse.ArgumentParser:
131
132
  description="Start a Flower client app",
132
133
  )
133
134
 
134
- parser.add_argument(
135
- "client-app",
136
- help="For example: `client:app` or `project.package.module:wrapper.app`",
137
- )
138
- parser.add_argument(
139
- "--insecure",
140
- action="store_true",
141
- help="Run the client without HTTPS. By default, the client runs with "
142
- "HTTPS enabled. Use this flag only if you understand the risks.",
143
- )
144
- parser.add_argument(
145
- "--rest",
146
- action="store_true",
147
- help="Use REST as a transport layer for the client.",
148
- )
149
- parser.add_argument(
150
- "--root-certificates",
151
- metavar="ROOT_CERT",
152
- type=str,
153
- help="Specifies the path to the PEM-encoded root certificate file for "
154
- "establishing secure HTTPS connections.",
155
- )
156
- parser.add_argument(
157
- "--server",
158
- default="0.0.0.0:9092",
159
- help="Server address",
160
- )
161
- parser.add_argument(
162
- "--max-retries",
163
- type=int,
164
- default=None,
165
- help="The maximum number of times the client will try to connect to the"
166
- "server before giving up in case of a connection error. By default,"
167
- "it is set to None, meaning there is no limit to the number of tries.",
168
- )
169
- parser.add_argument(
170
- "--max-wait-time",
171
- type=float,
172
- default=None,
173
- help="The maximum duration before the client stops trying to"
174
- "connect to the server in case of connection error. By default, it"
175
- "is set to None, meaning there is no limit to the total time.",
176
- )
177
- parser.add_argument(
178
- "--dir",
179
- default="",
180
- help="Add specified directory to the PYTHONPATH and load Flower "
181
- "app from there."
182
- " Default: current working directory.",
183
- )
135
+ parse_args_run_client_app(parser=parser)
184
136
 
185
137
  return parser
186
138
 
@@ -442,7 +394,8 @@ def _start_client_internal(
442
394
  grpc_max_message_length,
443
395
  root_certificates,
444
396
  ) as conn:
445
- receive, send, create_node, delete_node = conn
397
+ # pylint: disable-next=W0612
398
+ receive, send, create_node, delete_node, get_run = conn
446
399
 
447
400
  # Register node
448
401
  if create_node is not None:
@@ -660,6 +613,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
660
613
  Callable[[Message], None],
661
614
  Optional[Callable[[], None]],
662
615
  Optional[Callable[[], None]],
616
+ Optional[Callable[[int], Tuple[str, str]]],
663
617
  ]
664
618
  ],
665
619
  ],
@@ -68,6 +68,7 @@ def grpc_connection( # pylint: disable=R0915
68
68
  Callable[[Message], None],
69
69
  Optional[Callable[[], None]],
70
70
  Optional[Callable[[], None]],
71
+ Optional[Callable[[int], Tuple[str, str]]],
71
72
  ]
72
73
  ]:
73
74
  """Establish a gRPC connection to a gRPC server.
@@ -224,7 +225,7 @@ def grpc_connection( # pylint: disable=R0915
224
225
 
225
226
  try:
226
227
  # Yield methods
227
- yield (receive, send, None, None)
228
+ yield (receive, send, None, None, None)
228
229
  finally:
229
230
  # Make sure to have a final
230
231
  channel.close()
@@ -41,6 +41,8 @@ from flwr.common.serde import message_from_taskins, message_to_taskres
41
41
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
42
42
  CreateNodeRequest,
43
43
  DeleteNodeRequest,
44
+ GetRunRequest,
45
+ GetRunResponse,
44
46
  PingRequest,
45
47
  PingResponse,
46
48
  PullTaskInsRequest,
@@ -69,6 +71,7 @@ def grpc_request_response( # pylint: disable=R0914, R0915
69
71
  Callable[[Message], None],
70
72
  Optional[Callable[[], None]],
71
73
  Optional[Callable[[], None]],
74
+ Optional[Callable[[int], Tuple[str, str]]],
72
75
  ]
73
76
  ]:
74
77
  """Primitives for request/response-based interaction with a server.
@@ -122,7 +125,7 @@ def grpc_request_response( # pylint: disable=R0914, R0915
122
125
  ping_stop_event = threading.Event()
123
126
 
124
127
  ###########################################################################
125
- # ping/create_node/delete_node/receive/send functions
128
+ # ping/create_node/delete_node/receive/send/get_run functions
126
129
  ###########################################################################
127
130
 
128
131
  def ping() -> None:
@@ -241,8 +244,19 @@ def grpc_request_response( # pylint: disable=R0914, R0915
241
244
  # Cleanup
242
245
  metadata = None
243
246
 
247
+ def get_run(run_id: int) -> Tuple[str, str]:
248
+ # Call FleetAPI
249
+ get_run_request = GetRunRequest(run_id=run_id)
250
+ get_run_response: GetRunResponse = retry_invoker.invoke(
251
+ stub.GetRun,
252
+ request=get_run_request,
253
+ )
254
+
255
+ # Return fab_id and fab_version
256
+ return get_run_response.run.fab_id, get_run_response.run.fab_version
257
+
244
258
  try:
245
259
  # Yield methods
246
- yield (receive, send, create_node, delete_node)
260
+ yield (receive, send, create_node, delete_node, get_run)
247
261
  except Exception as exc: # pylint: disable=broad-except
248
262
  log(ERROR, exc)