flwr-nightly 1.16.0.dev20250225__py3-none-any.whl → 1.16.0.dev20250227__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.
flwr/client/client_app.py CHANGED
@@ -159,11 +159,15 @@ class ClientApp:
159
159
  # Message type did not match one of the known message types abvoe
160
160
  raise ValueError(f"Unknown message_type: {message.metadata.message_type}")
161
161
 
162
- def train(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
162
+ def train(
163
+ self, mods: Optional[list[Mod]] = None
164
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
163
165
  """Return a decorator that registers the train fn with the client app.
164
166
 
165
167
  Examples
166
168
  --------
169
+ Registering a train function:
170
+
167
171
  >>> app = ClientApp()
168
172
  >>>
169
173
  >>> @app.train()
@@ -171,6 +175,17 @@ class ClientApp:
171
175
  >>> print("ClientApp training running")
172
176
  >>> # Create and return an echo reply message
173
177
  >>> return message.create_reply(content=message.content())
178
+
179
+ Registering a train function with a function-specific modifier:
180
+
181
+ >>> from flwr.client.mod import message_size_mod
182
+ >>>
183
+ >>> app = ClientApp()
184
+ >>>
185
+ >>> @app.train(mods=[message_size_mod])
186
+ >>> def train(message: Message, context: Context) -> Message:
187
+ >>> print("ClientApp training running with message size mod")
188
+ >>> return message.create_reply(content=message.content())
174
189
  """
175
190
 
176
191
  def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
@@ -182,18 +197,22 @@ class ClientApp:
182
197
 
183
198
  # Register provided function with the ClientApp object
184
199
  # Wrap mods around the wrapped step function
185
- self._train = make_ffn(train_fn, self._mods)
200
+ self._train = make_ffn(train_fn, self._mods + (mods or []))
186
201
 
187
202
  # Return provided function unmodified
188
203
  return train_fn
189
204
 
190
205
  return train_decorator
191
206
 
192
- def evaluate(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
207
+ def evaluate(
208
+ self, mods: Optional[list[Mod]] = None
209
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
193
210
  """Return a decorator that registers the evaluate fn with the client app.
194
211
 
195
212
  Examples
196
213
  --------
214
+ Registering an evaluate function:
215
+
197
216
  >>> app = ClientApp()
198
217
  >>>
199
218
  >>> @app.evaluate()
@@ -201,6 +220,18 @@ class ClientApp:
201
220
  >>> print("ClientApp evaluation running")
202
221
  >>> # Create and return an echo reply message
203
222
  >>> return message.create_reply(content=message.content())
223
+
224
+ Registering an evaluate function with a function-specific modifier:
225
+
226
+ >>> from flwr.client.mod import message_size_mod
227
+ >>>
228
+ >>> app = ClientApp()
229
+ >>>
230
+ >>> @app.evaluate(mods=[message_size_mod])
231
+ >>> def evaluate(message: Message, context: Context) -> Message:
232
+ >>> print("ClientApp evaluation running with message size mod")
233
+ >>> # Create and return an echo reply message
234
+ >>> return message.create_reply(content=message.content())
204
235
  """
205
236
 
206
237
  def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
@@ -212,18 +243,22 @@ class ClientApp:
212
243
 
213
244
  # Register provided function with the ClientApp object
214
245
  # Wrap mods around the wrapped step function
215
- self._evaluate = make_ffn(evaluate_fn, self._mods)
246
+ self._evaluate = make_ffn(evaluate_fn, self._mods + (mods or []))
216
247
 
217
248
  # Return provided function unmodified
218
249
  return evaluate_fn
219
250
 
220
251
  return evaluate_decorator
221
252
 
222
- def query(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
253
+ def query(
254
+ self, mods: Optional[list[Mod]] = None
255
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
223
256
  """Return a decorator that registers the query fn with the client app.
224
257
 
225
258
  Examples
226
259
  --------
260
+ Registering a query function:
261
+
227
262
  >>> app = ClientApp()
228
263
  >>>
229
264
  >>> @app.query()
@@ -231,6 +266,18 @@ class ClientApp:
231
266
  >>> print("ClientApp query running")
232
267
  >>> # Create and return an echo reply message
233
268
  >>> return message.create_reply(content=message.content())
269
+
270
+ Registering a query function with a function-specific modifier:
271
+
272
+ >>> from flwr.client.mod import message_size_mod
273
+ >>>
274
+ >>> app = ClientApp()
275
+ >>>
276
+ >>> @app.query(mods=[message_size_mod])
277
+ >>> def query(message: Message, context: Context) -> Message:
278
+ >>> print("ClientApp query running with message size mod")
279
+ >>> # Create and return an echo reply message
280
+ >>> return message.create_reply(content=message.content())
234
281
  """
235
282
 
236
283
  def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
@@ -242,7 +289,7 @@ class ClientApp:
242
289
 
243
290
  # Register provided function with the ClientApp object
244
291
  # Wrap mods around the wrapped step function
245
- self._query = make_ffn(query_fn, self._mods)
292
+ self._query = make_ffn(query_fn, self._mods + (mods or []))
246
293
 
247
294
  # Return provided function unmodified
248
295
  return query_fn
@@ -20,6 +20,7 @@ from collections.abc import Sequence
20
20
  from pathlib import Path
21
21
  from typing import Optional, Union
22
22
 
23
+ from flwr.common.typing import UserInfo
23
24
  from flwr.proto.exec_pb2_grpc import ExecStub
24
25
 
25
26
  from ..typing import UserAuthCredentials, UserAuthLoginDetails
@@ -49,7 +50,7 @@ class ExecAuthPlugin(ABC):
49
50
  @abstractmethod
50
51
  def validate_tokens_in_metadata(
51
52
  self, metadata: Sequence[tuple[str, Union[str, bytes]]]
52
- ) -> bool:
53
+ ) -> tuple[bool, Optional[UserInfo]]:
53
54
  """Validate authentication tokens in the provided metadata."""
54
55
 
55
56
  @abstractmethod
flwr/common/constant.py CHANGED
@@ -212,3 +212,19 @@ class AuthType:
212
212
  def __new__(cls) -> AuthType:
213
213
  """Prevent instantiation."""
214
214
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
215
+
216
+
217
+ class EventLogWriterType:
218
+ """Event log writer types."""
219
+
220
+ FALSE = "false"
221
+ STDOUT = "stdout"
222
+
223
+ def __new__(cls) -> EventLogWriterType:
224
+ """Prevent instantiation."""
225
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
226
+
227
+ @classmethod
228
+ def choices(cls) -> list[str]:
229
+ """Return a list of available log writer choices."""
230
+ return [cls.FALSE, cls.STDOUT]
@@ -0,0 +1,26 @@
1
+ # Copyright 2025 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
+ """Event log plugin components."""
16
+
17
+
18
+ from .event_log_plugin import EventLogRequest as EventLogRequest
19
+ from .event_log_plugin import EventLogResponse as EventLogResponse
20
+ from .event_log_plugin import EventLogWriterPlugin as EventLogWriterPlugin
21
+
22
+ __all__ = [
23
+ "EventLogRequest",
24
+ "EventLogResponse",
25
+ "EventLogWriterPlugin",
26
+ ]
@@ -0,0 +1,87 @@
1
+ # Copyright 2025 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
+ """Abstract class for Flower Event Log Writer Plugin."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import Union
20
+
21
+ import grpc
22
+
23
+ from flwr.common.typing import LogEntry, UserInfo
24
+ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
25
+ GetLoginDetailsRequest,
26
+ GetLoginDetailsResponse,
27
+ ListRunsRequest,
28
+ ListRunsResponse,
29
+ StartRunRequest,
30
+ StartRunResponse,
31
+ StopRunRequest,
32
+ StopRunResponse,
33
+ StreamLogsRequest,
34
+ StreamLogsResponse,
35
+ )
36
+
37
+ # Type variables for request and response messages
38
+ EventLogRequest = Union[
39
+ StartRunRequest,
40
+ ListRunsRequest,
41
+ StreamLogsRequest,
42
+ StopRunRequest,
43
+ GetLoginDetailsRequest,
44
+ ]
45
+ EventLogResponse = Union[
46
+ StartRunResponse,
47
+ ListRunsResponse,
48
+ StreamLogsResponse,
49
+ StopRunResponse,
50
+ GetLoginDetailsResponse,
51
+ ]
52
+
53
+
54
+ class EventLogWriterPlugin(ABC):
55
+ """Abstract Flower Event Log Writer Plugin class for ExecServicer."""
56
+
57
+ @abstractmethod
58
+ def __init__(self) -> None:
59
+ """Abstract constructor."""
60
+
61
+ @abstractmethod
62
+ def compose_log_before_event( # pylint: disable=too-many-arguments
63
+ self,
64
+ request: EventLogRequest,
65
+ context: grpc.ServicerContext,
66
+ user_info: UserInfo,
67
+ method_name: str,
68
+ ) -> LogEntry:
69
+ """Compose pre-event log entry from the provided request and context."""
70
+
71
+ @abstractmethod
72
+ def compose_log_after_event( # pylint: disable=too-many-arguments,R0917
73
+ self,
74
+ request: EventLogRequest,
75
+ context: grpc.ServicerContext,
76
+ user_info: UserInfo,
77
+ method_name: str,
78
+ response: EventLogResponse,
79
+ ) -> LogEntry:
80
+ """Compose post-event log entry from the provided response and context."""
81
+
82
+ @abstractmethod
83
+ def write_log(
84
+ self,
85
+ log_entry: LogEntry,
86
+ ) -> None:
87
+ """Write the event log to the specified data sink."""
@@ -15,26 +15,17 @@
15
15
  """Conversion utility functions for Records."""
16
16
 
17
17
 
18
- from io import BytesIO
19
-
20
- import numpy as np
21
-
22
- from ..constant import SType
18
+ from ..logger import warn_deprecated_feature
23
19
  from ..typing import NDArray
24
20
  from .parametersrecord import Array
25
21
 
22
+ WARN_DEPRECATED_MESSAGE = (
23
+ "`array_from_numpy` is deprecated. Instead, use the `Array(ndarray)` class "
24
+ "directly or `Array.from_numpy_ndarray(ndarray)`."
25
+ )
26
+
26
27
 
27
28
  def array_from_numpy(ndarray: NDArray) -> Array:
28
29
  """Create Array from NumPy ndarray."""
29
- buffer = BytesIO()
30
- # WARNING: NEVER set allow_pickle to true.
31
- # Reason: loading pickled data can execute arbitrary code
32
- # Source: https://numpy.org/doc/stable/reference/generated/numpy.save.html
33
- np.save(buffer, ndarray, allow_pickle=False)
34
- data = buffer.getvalue()
35
- return Array(
36
- dtype=str(ndarray.dtype),
37
- shape=list(ndarray.shape),
38
- stype=SType.NUMPY,
39
- data=data,
40
- )
30
+ warn_deprecated_feature(WARN_DEPRECATED_MESSAGE)
31
+ return Array.from_numpy_ndarray(ndarray)
@@ -15,10 +15,12 @@
15
15
  """ParametersRecord and Array."""
16
16
 
17
17
 
18
+ from __future__ import annotations
19
+
18
20
  from collections import OrderedDict
19
21
  from dataclasses import dataclass
20
22
  from io import BytesIO
21
- from typing import Optional, cast
23
+ from typing import Any, cast, overload
22
24
 
23
25
  import numpy as np
24
26
 
@@ -27,29 +29,64 @@ from ..typing import NDArray
27
29
  from .typeddict import TypedDict
28
30
 
29
31
 
32
+ def _raise_array_init_error() -> None:
33
+ raise TypeError(
34
+ f"Invalid arguments for {Array.__qualname__}. Expected either a "
35
+ "NumPy ndarray, or explicit dtype/shape/stype/data values."
36
+ )
37
+
38
+
30
39
  @dataclass
31
40
  class Array:
32
41
  """Array type.
33
42
 
34
43
  A dataclass containing serialized data from an array-like or tensor-like object
35
- along with some metadata about it.
44
+ along with metadata about it. The class can be initialized in one of two ways:
45
+
46
+ 1. By specifying explicit values for `dtype`, `shape`, `stype`, and `data`.
47
+ 2. By providing a NumPy ndarray (via the `ndarray` argument).
48
+
49
+ In scenario (2), the `dtype`, `shape`, `stype`, and `data` are automatically
50
+ derived from the input. In scenario (1), these fields must be specified manually.
36
51
 
37
52
  Parameters
38
53
  ----------
39
- dtype : str
40
- A string representing the data type of the serialised object (e.g. `np.float32`)
54
+ dtype : Optional[str] (default: None)
55
+ A string representing the data type of the serialized object (e.g. `"float32"`).
56
+ Only required if you are not passing in a ndarray.
41
57
 
42
- shape : List[int]
43
- A list representing the shape of the unserialized array-like object. This is
44
- used to deserialize the data (depending on the serialization method) or simply
45
- as a metadata field.
58
+ shape : Optional[list[int]] (default: None)
59
+ A list representing the shape of the unserialized array-like object. Only
60
+ required if you are not passing in a ndarray.
46
61
 
47
- stype : str
48
- A string indicating the type of serialisation mechanism used to generate the
49
- bytes in `data` from an array-like or tensor-like object.
62
+ stype : Optional[str] (default: None)
63
+ A string indicating the serialization mechanism used to generate the bytes in
64
+ `data` from an array-like or tensor-like object. Only required if you are not
65
+ passing in a ndarray.
50
66
 
51
- data: bytes
52
- A buffer of bytes containing the data.
67
+ data : Optional[bytes] (default: None)
68
+ A buffer of bytes containing the data. Only required if you are not passing in
69
+ a ndarray.
70
+
71
+ ndarray : Optional[NDArray] (default: None)
72
+ A NumPy ndarray. If provided, the `dtype`, `shape`, `stype`, and `data`
73
+ fields are derived automatically from it.
74
+
75
+ Examples
76
+ --------
77
+ Initializing by specifying all fields directly:
78
+
79
+ >>> arr1 = Array(
80
+ >>> dtype="float32",
81
+ >>> shape=[3, 3],
82
+ >>> stype="numpy.ndarray",
83
+ >>> data=b"serialized_data...",
84
+ >>> )
85
+
86
+ Initializing with a NumPy ndarray:
87
+
88
+ >>> import numpy as np
89
+ >>> arr2 = Array(np.random.randn(3, 3))
53
90
  """
54
91
 
55
92
  dtype: str
@@ -57,6 +94,105 @@ class Array:
57
94
  stype: str
58
95
  data: bytes
59
96
 
97
+ @overload
98
+ def __init__( # noqa: E704
99
+ self, dtype: str, shape: list[int], stype: str, data: bytes
100
+ ) -> None: ...
101
+
102
+ @overload
103
+ def __init__(self, ndarray: NDArray) -> None: ... # noqa: E704
104
+
105
+ def __init__( # pylint: disable=too-many-arguments, too-many-locals
106
+ self,
107
+ *args: Any,
108
+ dtype: str | None = None,
109
+ shape: list[int] | None = None,
110
+ stype: str | None = None,
111
+ data: bytes | None = None,
112
+ ndarray: NDArray | None = None,
113
+ ) -> None:
114
+ # Determine the initialization method and validate input arguments.
115
+ # Support two initialization formats:
116
+ # 1. Array(dtype: str, shape: list[int], stype: str, data: bytes)
117
+ # 2. Array(ndarray: NDArray)
118
+
119
+ # Initialize all arguments
120
+ # If more than 4 positional arguments are provided, raise an error.
121
+ if len(args) > 4:
122
+ _raise_array_init_error()
123
+ all_args = [None] * 4
124
+ for i, arg in enumerate(args):
125
+ all_args[i] = arg
126
+ init_method: str | None = None # Track which init method is being used
127
+
128
+ # Try to assign a value to all_args[index] if it's not already set.
129
+ # If an initialization method is provided, update init_method.
130
+ def _try_set_arg(index: int, arg: Any, method: str) -> None:
131
+ # Skip if arg is None
132
+ if arg is None:
133
+ return
134
+ # Raise an error if all_args[index] is already set
135
+ if all_args[index] is not None:
136
+ _raise_array_init_error()
137
+ # Raise an error if a different initialization method is already set
138
+ nonlocal init_method
139
+ if init_method is not None and init_method != method:
140
+ _raise_array_init_error()
141
+ # Set init_method and all_args[index]
142
+ if init_method is None:
143
+ init_method = method
144
+ all_args[index] = arg
145
+
146
+ # Try to set keyword arguments in all_args
147
+ _try_set_arg(0, dtype, "direct")
148
+ _try_set_arg(1, shape, "direct")
149
+ _try_set_arg(2, stype, "direct")
150
+ _try_set_arg(3, data, "direct")
151
+ _try_set_arg(0, ndarray, "ndarray")
152
+
153
+ # Check if all arguments are correctly set
154
+ all_args = [arg for arg in all_args if arg is not None]
155
+
156
+ # Handle direct field initialization
157
+ if not init_method or init_method == "direct":
158
+ if (
159
+ len(all_args) == 4 # pylint: disable=too-many-boolean-expressions
160
+ and isinstance(all_args[0], str)
161
+ and isinstance(all_args[1], list)
162
+ and all(isinstance(i, int) for i in all_args[1])
163
+ and isinstance(all_args[2], str)
164
+ and isinstance(all_args[3], bytes)
165
+ ):
166
+ self.dtype, self.shape, self.stype, self.data = all_args
167
+ return
168
+
169
+ # Handle NumPy array
170
+ if not init_method or init_method == "ndarray":
171
+ if len(all_args) == 1 and isinstance(all_args[0], np.ndarray):
172
+ self.__dict__.update(self.from_numpy_ndarray(all_args[0]).__dict__)
173
+ return
174
+
175
+ _raise_array_init_error()
176
+
177
+ @classmethod
178
+ def from_numpy_ndarray(cls, ndarray: NDArray) -> Array:
179
+ """Create Array from NumPy ndarray."""
180
+ assert isinstance(
181
+ ndarray, np.ndarray
182
+ ), f"Expected NumPy ndarray, got {type(ndarray)}"
183
+ buffer = BytesIO()
184
+ # WARNING: NEVER set allow_pickle to true.
185
+ # Reason: loading pickled data can execute arbitrary code
186
+ # Source: https://numpy.org/doc/stable/reference/generated/numpy.save.html
187
+ np.save(buffer, ndarray, allow_pickle=False)
188
+ data = buffer.getvalue()
189
+ return Array(
190
+ dtype=str(ndarray.dtype),
191
+ shape=list(ndarray.shape),
192
+ stype=SType.NUMPY,
193
+ data=data,
194
+ )
195
+
60
196
  def numpy(self) -> NDArray:
61
197
  """Return the array as a NumPy array."""
62
198
  if self.stype != SType.NUMPY:
@@ -117,7 +253,6 @@ class ParametersRecord(TypedDict[str, Array]):
117
253
 
118
254
  >>> import numpy as np
119
255
  >>> from flwr.common import ParametersRecord
120
- >>> from flwr.common import array_from_numpy
121
256
  >>>
122
257
  >>> # Let's create a simple NumPy array
123
258
  >>> arr_np = np.random.randn(3, 3)
@@ -128,7 +263,7 @@ class ParametersRecord(TypedDict[str, Array]):
128
263
  >>> [-0.10758364, 1.97619858, -0.37120501]])
129
264
  >>>
130
265
  >>> # Let's create an Array out of it
131
- >>> arr = array_from_numpy(arr_np)
266
+ >>> arr = Array(arr_np)
132
267
  >>>
133
268
  >>> # If we print it you'll see (note the binary data)
134
269
  >>> Array(dtype='float64', shape=[3,3], stype='numpy.ndarray', data=b'@\x99\x18...')
@@ -176,7 +311,7 @@ class ParametersRecord(TypedDict[str, Array]):
176
311
 
177
312
  def __init__(
178
313
  self,
179
- array_dict: Optional[OrderedDict[str, Array]] = None,
314
+ array_dict: OrderedDict[str, Array] | None = None,
180
315
  keep_input: bool = False,
181
316
  ) -> None:
182
317
  super().__init__(_check_key, _check_value)
@@ -17,82 +17,79 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from dataclasses import dataclass
21
- from typing import cast
20
+ from logging import WARN
21
+ from textwrap import indent
22
+ from typing import TypeVar, Union, cast
22
23
 
24
+ from ..logger import log
23
25
  from .configsrecord import ConfigsRecord
24
26
  from .metricsrecord import MetricsRecord
25
27
  from .parametersrecord import ParametersRecord
26
28
  from .typeddict import TypedDict
27
29
 
30
+ RecordType = Union[ParametersRecord, MetricsRecord, ConfigsRecord]
28
31
 
29
- @dataclass
30
- class RecordSetData:
31
- """Inner data container for the RecordSet class."""
32
+ T = TypeVar("T")
32
33
 
33
- parameters_records: TypedDict[str, ParametersRecord]
34
- metrics_records: TypedDict[str, MetricsRecord]
35
- configs_records: TypedDict[str, ConfigsRecord]
36
34
 
37
- def __init__(
38
- self,
39
- parameters_records: dict[str, ParametersRecord] | None = None,
40
- metrics_records: dict[str, MetricsRecord] | None = None,
41
- configs_records: dict[str, ConfigsRecord] | None = None,
42
- ) -> None:
43
- self.parameters_records = TypedDict[str, ParametersRecord](
44
- self._check_fn_str, self._check_fn_params
45
- )
46
- self.metrics_records = TypedDict[str, MetricsRecord](
47
- self._check_fn_str, self._check_fn_metrics
35
+ def _check_key(key: str) -> None:
36
+ if not isinstance(key, str):
37
+ raise TypeError(
38
+ f"Expected `{str.__name__}`, but "
39
+ f"received `{type(key).__name__}` for the key."
48
40
  )
49
- self.configs_records = TypedDict[str, ConfigsRecord](
50
- self._check_fn_str, self._check_fn_configs
41
+
42
+
43
+ def _check_value(value: RecordType) -> None:
44
+ if not isinstance(value, (ParametersRecord, MetricsRecord, ConfigsRecord)):
45
+ raise TypeError(
46
+ f"Expected `{ParametersRecord.__name__}`, `{MetricsRecord.__name__}`, "
47
+ f"or `{ConfigsRecord.__name__}` but received "
48
+ f"`{type(value).__name__}` for the value."
51
49
  )
52
- if parameters_records is not None:
53
- self.parameters_records.update(parameters_records)
54
- if metrics_records is not None:
55
- self.metrics_records.update(metrics_records)
56
- if configs_records is not None:
57
- self.configs_records.update(configs_records)
58
-
59
- def _check_fn_str(self, key: str) -> None:
60
- if not isinstance(key, str):
61
- raise TypeError(
62
- f"Expected `{str.__name__}`, but "
63
- f"received `{type(key).__name__}` for the key."
64
- )
65
50
 
66
- def _check_fn_params(self, record: ParametersRecord) -> None:
67
- if not isinstance(record, ParametersRecord):
68
- raise TypeError(
69
- f"Expected `{ParametersRecord.__name__}`, but "
70
- f"received `{type(record).__name__}` for the value."
71
- )
72
51
 
73
- def _check_fn_metrics(self, record: MetricsRecord) -> None:
74
- if not isinstance(record, MetricsRecord):
75
- raise TypeError(
76
- f"Expected `{MetricsRecord.__name__}`, but "
77
- f"received `{type(record).__name__}` for the value."
78
- )
52
+ class _SyncedDict(TypedDict[str, T]):
53
+ """A synchronized dictionary that mirrors changes to an underlying RecordSet.
79
54
 
80
- def _check_fn_configs(self, record: ConfigsRecord) -> None:
81
- if not isinstance(record, ConfigsRecord):
55
+ This dictionary ensures that any modifications (set or delete operations)
56
+ are automatically reflected in the associated `RecordSet`. Only values of
57
+ the specified `allowed_type` are permitted.
58
+ """
59
+
60
+ def __init__(self, ref_recordset: RecordSet, allowed_type: type[T]) -> None:
61
+ if not issubclass(
62
+ allowed_type, (ParametersRecord, MetricsRecord, ConfigsRecord)
63
+ ):
64
+ raise TypeError(f"{allowed_type} is not a valid type.")
65
+ super().__init__(_check_key, self.check_value)
66
+ self.recordset = ref_recordset
67
+ self.allowed_type = allowed_type
68
+
69
+ def __setitem__(self, key: str, value: T) -> None:
70
+ super().__setitem__(key, value)
71
+ self.recordset[key] = cast(RecordType, value)
72
+
73
+ def __delitem__(self, key: str) -> None:
74
+ super().__delitem__(key)
75
+ del self.recordset[key]
76
+
77
+ def check_value(self, value: T) -> None:
78
+ """Check if value is of expected type."""
79
+ if not isinstance(value, self.allowed_type):
82
80
  raise TypeError(
83
- f"Expected `{ConfigsRecord.__name__}`, but "
84
- f"received `{type(record).__name__}` for the value."
81
+ f"Expected `{self.allowed_type.__name__}`, but "
82
+ f"received `{type(value).__name__}` for the value."
85
83
  )
86
84
 
87
85
 
88
- class RecordSet:
86
+ class RecordSet(TypedDict[str, RecordType]):
89
87
  """RecordSet stores groups of parameters, metrics and configs.
90
88
 
91
- A :code:`RecordSet` is the unified mechanism by which parameters,
92
- metrics and configs can be either stored as part of a
93
- `flwr.common.Context <flwr.common.Context.html>`_ in your apps
94
- or communicated as part of a
95
- `flwr.common.Message <flwr.common.Message.html>`_ between your apps.
89
+ A :class:`RecordSet` is the unified mechanism by which parameters,
90
+ metrics and configs can be either stored as part of a :class:`Context`
91
+ in your apps or communicated as part of a :class:`Message` between
92
+ your apps.
96
93
 
97
94
  Parameters
98
95
  ----------
@@ -127,12 +124,12 @@ class RecordSet:
127
124
  >>> # We can create a ConfigsRecord
128
125
  >>> c_record = ConfigsRecord({"lr": 0.1, "batch-size": 128})
129
126
  >>> # Adding it to the record_set would look like this
130
- >>> my_recordset.configs_records["my_config"] = c_record
127
+ >>> my_recordset["my_config"] = c_record
131
128
  >>>
132
129
  >>> # We can create a MetricsRecord following a similar process
133
130
  >>> m_record = MetricsRecord({"accuracy": 0.93, "losses": [0.23, 0.1]})
134
131
  >>> # Adding it to the record_set would look like this
135
- >>> my_recordset.metrics_records["my_metrics"] = m_record
132
+ >>> my_recordset["my_metrics"] = m_record
136
133
 
137
134
  Adding a :code:`ParametersRecord` follows the same steps as above but first,
138
135
  the array needs to be serialized and represented as a :code:`flwr.common.Array`.
@@ -151,7 +148,7 @@ class RecordSet:
151
148
  >>> p_record = ParametersRecord({"my_array": arr})
152
149
  >>>
153
150
  >>> # Adding it to the record_set would look like this
154
- >>> my_recordset.parameters_records["my_parameters"] = p_record
151
+ >>> my_recordset["my_parameters"] = p_record
155
152
 
156
153
  For additional examples on how to construct each of the records types shown
157
154
  above, please refer to the documentation for :code:`ConfigsRecord`,
@@ -164,39 +161,57 @@ class RecordSet:
164
161
  metrics_records: dict[str, MetricsRecord] | None = None,
165
162
  configs_records: dict[str, ConfigsRecord] | None = None,
166
163
  ) -> None:
167
- data = RecordSetData(
168
- parameters_records=parameters_records,
169
- metrics_records=metrics_records,
170
- configs_records=configs_records,
171
- )
172
- self.__dict__["_data"] = data
164
+ super().__init__(_check_key, _check_value)
165
+ for key, p_record in (parameters_records or {}).items():
166
+ self[key] = p_record
167
+ for key, m_record in (metrics_records or {}).items():
168
+ self[key] = m_record
169
+ for key, c_record in (configs_records or {}).items():
170
+ self[key] = c_record
173
171
 
174
172
  @property
175
173
  def parameters_records(self) -> TypedDict[str, ParametersRecord]:
176
- """Dictionary holding ParametersRecord instances."""
177
- data = cast(RecordSetData, self.__dict__["_data"])
178
- return data.parameters_records
174
+ """Dictionary holding only ParametersRecord instances."""
175
+ synced_dict = _SyncedDict[ParametersRecord](self, ParametersRecord)
176
+ for key, record in self.items():
177
+ if isinstance(record, ParametersRecord):
178
+ synced_dict[key] = record
179
+ return synced_dict
179
180
 
180
181
  @property
181
182
  def metrics_records(self) -> TypedDict[str, MetricsRecord]:
182
- """Dictionary holding MetricsRecord instances."""
183
- data = cast(RecordSetData, self.__dict__["_data"])
184
- return data.metrics_records
183
+ """Dictionary holding only MetricsRecord instances."""
184
+ synced_dict = _SyncedDict[MetricsRecord](self, MetricsRecord)
185
+ for key, record in self.items():
186
+ if isinstance(record, MetricsRecord):
187
+ synced_dict[key] = record
188
+ return synced_dict
185
189
 
186
190
  @property
187
191
  def configs_records(self) -> TypedDict[str, ConfigsRecord]:
188
- """Dictionary holding ConfigsRecord instances."""
189
- data = cast(RecordSetData, self.__dict__["_data"])
190
- return data.configs_records
192
+ """Dictionary holding only ConfigsRecord instances."""
193
+ synced_dict = _SyncedDict[ConfigsRecord](self, ConfigsRecord)
194
+ for key, record in self.items():
195
+ if isinstance(record, ConfigsRecord):
196
+ synced_dict[key] = record
197
+ return synced_dict
191
198
 
192
199
  def __repr__(self) -> str:
193
200
  """Return a string representation of this instance."""
194
201
  flds = ("parameters_records", "metrics_records", "configs_records")
195
- view = ", ".join([f"{fld}={getattr(self, fld)!r}" for fld in flds])
196
- return f"{self.__class__.__qualname__}({view})"
197
-
198
- def __eq__(self, other: object) -> bool:
199
- """Compare two instances of the class."""
200
- if not isinstance(other, self.__class__):
201
- raise NotImplementedError
202
- return self.__dict__ == other.__dict__
202
+ fld_views = [f"{fld}={dict(getattr(self, fld))!r}" for fld in flds]
203
+ view = indent(",\n".join(fld_views), " ")
204
+ return f"{self.__class__.__qualname__}(\n{view}\n)"
205
+
206
+ def __setitem__(self, key: str, value: RecordType) -> None:
207
+ """Set the given key to the given value after type checking."""
208
+ original_value = self.get(key, None)
209
+ super().__setitem__(key, value)
210
+ if original_value is not None and not isinstance(value, type(original_value)):
211
+ log(
212
+ WARN,
213
+ "Key '%s' was overwritten: record of type `%s` replaced with type `%s`",
214
+ key,
215
+ type(original_value).__name__,
216
+ type(value).__name__,
217
+ )
@@ -25,7 +25,11 @@ from flwr.common.typing import NDArrayFloat, NDArrayInt
25
25
  def _stochastic_round(arr: NDArrayFloat) -> NDArrayInt:
26
26
  ret: NDArrayInt = np.ceil(arr).astype(np.int32)
27
27
  rand_arr = np.random.rand(*ret.shape)
28
- ret[rand_arr < ret - arr] -= 1
28
+ if len(ret.shape) == 0:
29
+ if rand_arr < ret - arr:
30
+ ret -= 1
31
+ else:
32
+ ret[rand_arr < ret - arr] -= 1
29
33
  return ret
30
34
 
31
35
 
flwr/common/typing.py CHANGED
@@ -286,3 +286,39 @@ class UserAuthCredentials:
286
286
 
287
287
  access_token: str
288
288
  refresh_token: str
289
+
290
+
291
+ @dataclass
292
+ class UserInfo:
293
+ """User information for event log."""
294
+
295
+ user_id: Optional[str]
296
+ user_name: Optional[str]
297
+
298
+
299
+ @dataclass
300
+ class Actor:
301
+ """Event log actor."""
302
+
303
+ actor_id: Optional[str]
304
+ description: Optional[str]
305
+ ip_address: str
306
+
307
+
308
+ @dataclass
309
+ class Event:
310
+ """Event log description."""
311
+
312
+ action: str
313
+ run_id: Optional[int]
314
+ fab_hash: Optional[str]
315
+
316
+
317
+ @dataclass
318
+ class LogEntry:
319
+ """Event log record."""
320
+
321
+ timestamp: str
322
+ actor: Actor
323
+ event: Event
324
+ status: str
flwr/server/app.py CHANGED
@@ -90,7 +90,11 @@ BASE_DIR = get_flwr_dir() / "superlink" / "ffs"
90
90
 
91
91
 
92
92
  try:
93
- from flwr.ee import add_ee_args_superlink, get_exec_auth_plugins
93
+ from flwr.ee import (
94
+ add_ee_args_superlink,
95
+ get_dashboard_server,
96
+ get_exec_auth_plugins,
97
+ )
94
98
  except ImportError:
95
99
 
96
100
  # pylint: disable-next=unused-argument
@@ -431,6 +435,17 @@ def run_superlink() -> None:
431
435
  scheduler_th.start()
432
436
  bckg_threads.append(scheduler_th)
433
437
 
438
+ # Add Dashboard server if available
439
+ if dashboard_address := getattr(args, "dashboard_address", None):
440
+ dashboard_address_str, _, _ = _format_address(dashboard_address)
441
+ dashboard_server = get_dashboard_server(
442
+ address=dashboard_address_str,
443
+ state_factory=state_factory,
444
+ certificates=None,
445
+ )
446
+
447
+ grpc_servers.append(dashboard_server)
448
+
434
449
  # Graceful shutdown
435
450
  register_exit_handlers(
436
451
  event_type=EventType.RUN_SUPERLINK_LEAVE,
@@ -85,7 +85,7 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
85
85
  tokens = self.auth_plugin.refresh_tokens(context.invocation_metadata())
86
86
  if tokens is not None:
87
87
  context.send_initial_metadata(tokens)
88
- return call(request, context) # type: ignore
88
+ return call(request, context)
89
89
 
90
90
  context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
91
91
  raise grpc.RpcError() # This line is unreachable
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.16.0.dev20250225
3
+ Version: 1.16.0.dev20250227
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -74,7 +74,7 @@ flwr/cli/utils.py,sha256=D9XcpxzwkGPNdwX16o0kI-sYnRDMlWYyKNIpz6npRhQ,11236
74
74
  flwr/client/__init__.py,sha256=DGDoO0AEAfz-0CUFmLdyUUweAS64-07AOnmDfWUefK4,1192
75
75
  flwr/client/app.py,sha256=tNnef5wGVfqMiiGiWzAuULyy1QpvCKukiRmNi_a2cQc,34261
76
76
  flwr/client/client.py,sha256=8o58nd9o6ZFcMIaVYPGcV4MSjBG4H0oFgWiv8ZEO3oA,7895
77
- flwr/client/client_app.py,sha256=cTig-N00YzTucbo9zNi6I21J8PlbflU_8J_f5CI-Wpw,10390
77
+ flwr/client/client_app.py,sha256=Vv4rfDcV9ycb9ZuUkhT_8wX7W1GIrALwlvRcUeVel3Y,12161
78
78
  flwr/client/clientapp/__init__.py,sha256=kZqChGnTChQ1WGSUkIlW2S5bc0d0mzDubCAmZUGRpEY,800
79
79
  flwr/client/clientapp/app.py,sha256=Us5Mw3wvGd_6P1zHOf3TNcRGBBulVZDo3LuZOs17WgM,8963
80
80
  flwr/client/clientapp/clientappio_servicer.py,sha256=5L6bjw_j3Mnx9kRFwYwxDNABKurBO5q1jZOWE_X11wQ,8522
@@ -115,14 +115,16 @@ flwr/common/__init__.py,sha256=TVaoFEJE158aui1TPZQiJCDZX4RNHRyI8I55VC80HhI,3901
115
115
  flwr/common/address.py,sha256=rRaN1JpiCJnit7ImEqZVxURQ69dPihRoyyWn_3I2wh4,4119
116
116
  flwr/common/args.py,sha256=MgkTUXACuySHyNdxrb7-pK0_R-S2Q7W5MnE3onYUf5I,5183
117
117
  flwr/common/auth_plugin/__init__.py,sha256=1Y8Oj3iB49IHDu9tvDih1J74Ygu7k85V9s2A4WORPyA,887
118
- flwr/common/auth_plugin/auth_plugin.py,sha256=wgDorBUB4IkK6twQ8vNawRVz7BDPmKdXZBNLqhU9RSs,3871
118
+ flwr/common/auth_plugin/auth_plugin.py,sha256=dQU5U4uJIA5XqgOJ3PankHWq-uXCaMvO74khaMPGdiU,3938
119
119
  flwr/common/config.py,sha256=SAkG3BztnA6iupXxF3GAIpGmWVVCH0ptyMpC9yjr_14,13965
120
- flwr/common/constant.py,sha256=AdNCrHi4sgMCecdX7hHWxMFz3y9pWsjSGU25mA4OJyE,6619
120
+ flwr/common/constant.py,sha256=6NtDbh_RgQebtPfn01a8yN_7poMOuwy6jbtVLQdBbQc,7026
121
121
  flwr/common/context.py,sha256=uJ-mnoC_8y_udEb3kAX-r8CPphNTWM72z1AlsvQEu54,2403
122
122
  flwr/common/date.py,sha256=NHHpESce5wYqEwoDXf09gp9U9l_5Bmlh2BsOcwS-kDM,1554
123
123
  flwr/common/differential_privacy.py,sha256=YA01NqjddKNAEVmf7hXmOVxOjhekgzvJudk3mBGq-2k,6148
124
124
  flwr/common/differential_privacy_constants.py,sha256=c7b7tqgvT7yMK0XN9ndiTBs4mQf6d3qk6K7KBZGlV4Q,1074
125
125
  flwr/common/dp.py,sha256=vddkvyjV2FhRoN4VuU2LeAM1UBn7dQB8_W-Qdiveal8,1978
126
+ flwr/common/event_log_plugin/__init__.py,sha256=iLGSlmIta-qY4Jm5Os8IBl5cvVYXyFGlqkUiUXQDlU0,1017
127
+ flwr/common/event_log_plugin/event_log_plugin.py,sha256=OdyYsBTqhLRRC0HL3_hv29LXJvHyhLCANXcLUqgAFTI,2568
126
128
  flwr/common/exit/__init__.py,sha256=-ZOJYLaNnR729a7VzZiFsLiqngzKQh3xc27svYStZ_Q,826
127
129
  flwr/common/exit/exit.py,sha256=DmZFyksp-w1sFDQekq5Z-qfnr-ivCAv78aQkqj-TDps,3458
128
130
  flwr/common/exit/exit_code.py,sha256=PNEnCrZfOILjfDAFu5m-2YWEJBrk97xglq4zCUlqV7E,3470
@@ -135,10 +137,10 @@ flwr/common/parameter.py,sha256=-bFAUayToYDF50FZGrBC1hQYJCQDtB2bbr3ZuVLMtdE,2095
135
137
  flwr/common/pyproject.py,sha256=vEAxl800XiJ1JNJDui8vuVV-08msnB6hLt7o95viZl0,1386
136
138
  flwr/common/record/__init__.py,sha256=LUixpq0Z-lMJwCIu1-4u5HfvRPjRMRgoAc6YJQ6UEOs,1055
137
139
  flwr/common/record/configsrecord.py,sha256=i40jOzBx04ysZKECwaw4FdUXMdY9HgdY8GAqKdTO1Lw,6486
138
- flwr/common/record/conversion_utils.py,sha256=n3I3SI2P6hUjyxbWNc0QAch-SEhfMK6Hm-UUaplAlUc,1393
140
+ flwr/common/record/conversion_utils.py,sha256=ZcsM-vTm_rVtLXLFD2RY3N47V_hUr3ywTdtnpVXnOGU,1202
139
141
  flwr/common/record/metricsrecord.py,sha256=UywkEPbifiu_IyPUFoDJCi8WEVLujlqZERUWAWpc3vs,5752
140
- flwr/common/record/parametersrecord.py,sha256=SasHn35JRHsj8G1UT76FgRjaP4ZJasejvgjBV6HnaTg,7748
141
- flwr/common/record/recordset.py,sha256=qqIFdRZ0ivQhUhztpdxNIvCRDZQXY_zX0kKDEU9mhfM,8319
142
+ flwr/common/record/parametersrecord.py,sha256=rR0LbeNrKrdK37CiAA56Z5WBq-ZzZ2YNSUkcmr5i2lI,12950
143
+ flwr/common/record/recordset.py,sha256=gY3nmE--8sXSIPEFMReq7nh6hMiF-sr0HpfUtiB65E8,8890
142
144
  flwr/common/record/typeddict.py,sha256=q5hL2xkXymuiCprHWb69mUmLpWQk_XXQq0hGQ69YPaw,3599
143
145
  flwr/common/recordset_compat.py,sha256=ViSwA26h6Q55ZmV1LLjSJpcKiipV-p_JpCj4wxdE-Ow,14230
144
146
  flwr/common/retry_invoker.py,sha256=UIDKsn0AitS3fOr43WTqZAdD-TaHkBeTj1QxD7SGba0,14481
@@ -147,12 +149,12 @@ flwr/common/secure_aggregation/crypto/__init__.py,sha256=nlHesCWy8xxE5s6qHWnauCt
147
149
  flwr/common/secure_aggregation/crypto/shamir.py,sha256=wCSfEfeaPgJ9Om580-YPUF2ljiyRhq33TRC4HtwxYl8,2779
148
150
  flwr/common/secure_aggregation/crypto/symmetric_encryption.py,sha256=J_pRkxbogc7e1fxRZStZFBdzzG5jeUycshJPpvyCt6g,5333
149
151
  flwr/common/secure_aggregation/ndarrays_arithmetic.py,sha256=zvVAIrIyI6OSzGhpCi8NNaTvPXmoMYQIPJT-NkBg8RU,3013
150
- flwr/common/secure_aggregation/quantization.py,sha256=mC4uLf05zeONo8Ke-BY0Tj8UCMOS7VD93zHCzuv3MHU,2304
152
+ flwr/common/secure_aggregation/quantization.py,sha256=NE_ltC3Fx5Z3bMKqJHA95wQf2tkGQlN0VZf3d1w5ABA,2400
151
153
  flwr/common/secure_aggregation/secaggplus_constants.py,sha256=9MF-oQh62uD7rt9VeNB-rHf2gBLd5GL3S9OejCxmILY,2183
152
154
  flwr/common/secure_aggregation/secaggplus_utils.py,sha256=OgYd68YBRaHQYLc-YdExj9CSpwL58bVTaPrdHoAj2AE,3214
153
155
  flwr/common/serde.py,sha256=iDVS5IXGKqEuzQAvnroH9c6KFEHxKFEWmkGP89PMOO0,31064
154
156
  flwr/common/telemetry.py,sha256=APKVubU_zJNrE-M_rip6S6Fsu41DxY3tAjFWNOgTmC0,9086
155
- flwr/common/typing.py,sha256=IMgs_7nDJWn8Eb7y16Hl4HC3imQV7_Hdla1IFs3B5u8,6382
157
+ flwr/common/typing.py,sha256=Prl8_4tKnIl_Kh5UjJGbw1tnld543EkXrX0RWffJpiA,6900
156
158
  flwr/common/version.py,sha256=aNSxLL49RKeLz8sPcZrsTEWtrAeQ0uxu6tjmfba4O60,1325
157
159
  flwr/proto/__init__.py,sha256=hbY7JYakwZwCkYgCNlmHdc8rtvfoJbAZLalMdc--CGc,683
158
160
  flwr/proto/clientappio_pb2.py,sha256=aroQDv0D2GquQ5Ujqml7n7l6ObZoXqMvDa0XVO-_8Cc,3703
@@ -217,7 +219,7 @@ flwr/proto/transport_pb2_grpc.py,sha256=vLN3EHtx2aEEMCO4f1Upu-l27BPzd3-5pV-u8wPc
217
219
  flwr/proto/transport_pb2_grpc.pyi,sha256=AGXf8RiIiW2J5IKMlm_3qT3AzcDa4F3P5IqUjve_esA,766
218
220
  flwr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
221
  flwr/server/__init__.py,sha256=cEg1oecBu4cKB69iJCqWEylC8b5XW47bl7rQiJsdTvM,1528
220
- flwr/server/app.py,sha256=CVj7lDAYJ4GYnBvAIa0R3A8JV5Dt-ATxDOrLxafLFV8,30545
222
+ flwr/server/app.py,sha256=a_3hM-RktaPp3QTKDlBuHATB9oS2deGShZGEtmhlnX0,31005
221
223
  flwr/server/client_manager.py,sha256=7Ese0tgrH-i-ms363feYZJKwB8gWnXSmg_hYF2Bju4U,6227
222
224
  flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2399
223
225
  flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
@@ -324,11 +326,11 @@ flwr/superexec/app.py,sha256=Z6kYHWd62YL0Q4YKyCAbt_BcefNfbKH6V-jCC-1NkZM,1842
324
326
  flwr/superexec/deployment.py,sha256=wZ9G42gGS91knfplswh95MnQ83Fzu-rs6wcuNgDmmvY,6735
325
327
  flwr/superexec/exec_grpc.py,sha256=ttA9qoZzSLF0Mfk1L4hzOkMSNuj5rR58_kKBYwcyrAg,2864
326
328
  flwr/superexec/exec_servicer.py,sha256=X10ILT-AoGMrB3IgI2mBe9i-QcIVUAl9bucuqVOPYkU,8341
327
- flwr/superexec/exec_user_auth_interceptor.py,sha256=K06OU-l4LnYhTDg071hGJuOaQWEJbZsYi5qxUmmtiG0,3704
329
+ flwr/superexec/exec_user_auth_interceptor.py,sha256=XvLRxIsZ5m90pg6COx-tKkixFWxF-FaBeP3PqJFtksw,3688
328
330
  flwr/superexec/executor.py,sha256=_B55WW2TD1fBINpabSSDRenVHXYmvlfhv-k8hJKU4lQ,3115
329
331
  flwr/superexec/simulation.py,sha256=WQDon15oqpMopAZnwRZoTICYCfHqtkvFSqiTQ2hLD_g,4088
330
- flwr_nightly-1.16.0.dev20250225.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
331
- flwr_nightly-1.16.0.dev20250225.dist-info/METADATA,sha256=Sh3iV8qd0cS526-IF2D_U0_Lj5KJkCbzwiwtFLzVrkw,15877
332
- flwr_nightly-1.16.0.dev20250225.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
333
- flwr_nightly-1.16.0.dev20250225.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
334
- flwr_nightly-1.16.0.dev20250225.dist-info/RECORD,,
332
+ flwr_nightly-1.16.0.dev20250227.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
333
+ flwr_nightly-1.16.0.dev20250227.dist-info/METADATA,sha256=E--UFuJfhErjNYm6D4cvC1efJ_ZXAFNGII1jKTbC6mc,15877
334
+ flwr_nightly-1.16.0.dev20250227.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
335
+ flwr_nightly-1.16.0.dev20250227.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
336
+ flwr_nightly-1.16.0.dev20250227.dist-info/RECORD,,