flwr 1.15.2__py3-none-any.whl → 1.17.0__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.
Files changed (120) hide show
  1. flwr/cli/build.py +2 -0
  2. flwr/cli/log.py +20 -21
  3. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +1 -1
  4. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  5. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  12. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  13. flwr/cli/run/run.py +5 -9
  14. flwr/client/app.py +6 -4
  15. flwr/client/client_app.py +260 -86
  16. flwr/client/clientapp/app.py +6 -2
  17. flwr/client/grpc_client/connection.py +24 -21
  18. flwr/client/message_handler/message_handler.py +28 -28
  19. flwr/client/mod/__init__.py +2 -2
  20. flwr/client/mod/centraldp_mods.py +7 -7
  21. flwr/client/mod/comms_mods.py +16 -22
  22. flwr/client/mod/localdp_mod.py +4 -4
  23. flwr/client/mod/secure_aggregation/secaggplus_mod.py +31 -31
  24. flwr/client/rest_client/connection.py +4 -6
  25. flwr/client/run_info_store.py +2 -2
  26. flwr/client/supernode/__init__.py +0 -2
  27. flwr/client/supernode/app.py +1 -11
  28. flwr/common/__init__.py +12 -4
  29. flwr/common/address.py +35 -0
  30. flwr/common/args.py +8 -2
  31. flwr/common/auth_plugin/auth_plugin.py +2 -1
  32. flwr/common/config.py +4 -4
  33. flwr/common/constant.py +16 -0
  34. flwr/common/context.py +4 -4
  35. flwr/common/event_log_plugin/__init__.py +22 -0
  36. flwr/common/event_log_plugin/event_log_plugin.py +60 -0
  37. flwr/common/grpc.py +1 -1
  38. flwr/common/logger.py +2 -2
  39. flwr/common/message.py +338 -102
  40. flwr/common/object_ref.py +0 -10
  41. flwr/common/record/__init__.py +8 -4
  42. flwr/common/record/arrayrecord.py +626 -0
  43. flwr/common/record/{configsrecord.py → configrecord.py} +75 -29
  44. flwr/common/record/conversion_utils.py +9 -18
  45. flwr/common/record/{metricsrecord.py → metricrecord.py} +78 -32
  46. flwr/common/record/recorddict.py +288 -0
  47. flwr/common/recorddict_compat.py +410 -0
  48. flwr/common/secure_aggregation/quantization.py +5 -1
  49. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  50. flwr/common/serde.py +67 -190
  51. flwr/common/telemetry.py +0 -10
  52. flwr/common/typing.py +44 -8
  53. flwr/proto/exec_pb2.py +3 -3
  54. flwr/proto/exec_pb2.pyi +3 -3
  55. flwr/proto/message_pb2.py +12 -12
  56. flwr/proto/message_pb2.pyi +9 -9
  57. flwr/proto/recorddict_pb2.py +70 -0
  58. flwr/proto/{recordset_pb2.pyi → recorddict_pb2.pyi} +35 -35
  59. flwr/proto/run_pb2.py +31 -31
  60. flwr/proto/run_pb2.pyi +3 -3
  61. flwr/server/__init__.py +3 -1
  62. flwr/server/app.py +74 -3
  63. flwr/server/compat/__init__.py +2 -2
  64. flwr/server/compat/app.py +15 -12
  65. flwr/server/compat/app_utils.py +26 -18
  66. flwr/server/compat/{driver_client_proxy.py → grid_client_proxy.py} +41 -41
  67. flwr/server/fleet_event_log_interceptor.py +94 -0
  68. flwr/server/{driver → grid}/__init__.py +8 -7
  69. flwr/server/{driver/driver.py → grid/grid.py} +48 -19
  70. flwr/server/{driver/grpc_driver.py → grid/grpc_grid.py} +88 -56
  71. flwr/server/{driver/inmemory_driver.py → grid/inmemory_grid.py} +41 -54
  72. flwr/server/run_serverapp.py +6 -17
  73. flwr/server/server_app.py +126 -33
  74. flwr/server/serverapp/app.py +10 -10
  75. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +2 -2
  76. flwr/server/superlink/fleet/message_handler/message_handler.py +8 -12
  77. flwr/server/superlink/fleet/vce/backend/backend.py +3 -3
  78. flwr/server/superlink/fleet/vce/backend/raybackend.py +2 -2
  79. flwr/server/superlink/fleet/vce/vce_api.py +33 -38
  80. flwr/server/superlink/linkstate/in_memory_linkstate.py +171 -132
  81. flwr/server/superlink/linkstate/linkstate.py +51 -64
  82. flwr/server/superlink/linkstate/sqlite_linkstate.py +253 -285
  83. flwr/server/superlink/linkstate/utils.py +171 -133
  84. flwr/server/superlink/{driver → serverappio}/__init__.py +1 -1
  85. flwr/server/superlink/{driver → serverappio}/serverappio_grpc.py +1 -1
  86. flwr/server/superlink/{driver → serverappio}/serverappio_servicer.py +27 -29
  87. flwr/server/superlink/simulation/simulationio_servicer.py +2 -2
  88. flwr/server/typing.py +3 -3
  89. flwr/server/utils/__init__.py +2 -2
  90. flwr/server/utils/validator.py +53 -68
  91. flwr/server/workflow/default_workflows.py +52 -58
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +48 -50
  93. flwr/simulation/app.py +2 -2
  94. flwr/simulation/ray_transport/ray_actor.py +4 -2
  95. flwr/simulation/ray_transport/ray_client_proxy.py +34 -32
  96. flwr/simulation/run_simulation.py +15 -15
  97. flwr/superexec/app.py +0 -14
  98. flwr/superexec/deployment.py +4 -4
  99. flwr/superexec/exec_event_log_interceptor.py +135 -0
  100. flwr/superexec/exec_grpc.py +10 -4
  101. flwr/superexec/exec_servicer.py +6 -6
  102. flwr/superexec/exec_user_auth_interceptor.py +22 -4
  103. flwr/superexec/executor.py +3 -3
  104. flwr/superexec/simulation.py +3 -3
  105. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/METADATA +5 -5
  106. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/RECORD +111 -112
  107. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/entry_points.txt +0 -3
  108. flwr/client/message_handler/task_handler.py +0 -37
  109. flwr/common/record/parametersrecord.py +0 -204
  110. flwr/common/record/recordset.py +0 -202
  111. flwr/common/recordset_compat.py +0 -418
  112. flwr/proto/recordset_pb2.py +0 -70
  113. flwr/proto/task_pb2.py +0 -33
  114. flwr/proto/task_pb2.pyi +0 -100
  115. flwr/proto/task_pb2_grpc.py +0 -4
  116. flwr/proto/task_pb2_grpc.pyi +0 -4
  117. /flwr/proto/{recordset_pb2_grpc.py → recorddict_pb2_grpc.py} +0 -0
  118. /flwr/proto/{recordset_pb2_grpc.pyi → recorddict_pb2_grpc.pyi} +0 -0
  119. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/LICENSE +0 -0
  120. {flwr-1.15.2.dist-info → flwr-1.17.0.dist-info}/WHEEL +0 -0
@@ -1,204 +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
- """ParametersRecord and Array."""
16
-
17
-
18
- from collections import OrderedDict
19
- from dataclasses import dataclass
20
- from io import BytesIO
21
- from typing import Optional, cast
22
-
23
- import numpy as np
24
-
25
- from ..constant import SType
26
- from ..typing import NDArray
27
- from .typeddict import TypedDict
28
-
29
-
30
- @dataclass
31
- class Array:
32
- """Array type.
33
-
34
- A dataclass containing serialized data from an array-like or tensor-like object
35
- along with some metadata about it.
36
-
37
- Parameters
38
- ----------
39
- dtype : str
40
- A string representing the data type of the serialised object (e.g. `np.float32`)
41
-
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.
46
-
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.
50
-
51
- data: bytes
52
- A buffer of bytes containing the data.
53
- """
54
-
55
- dtype: str
56
- shape: list[int]
57
- stype: str
58
- data: bytes
59
-
60
- def numpy(self) -> NDArray:
61
- """Return the array as a NumPy array."""
62
- if self.stype != SType.NUMPY:
63
- raise TypeError(
64
- f"Unsupported serialization type for numpy conversion: '{self.stype}'"
65
- )
66
- bytes_io = BytesIO(self.data)
67
- # WARNING: NEVER set allow_pickle to true.
68
- # Reason: loading pickled data can execute arbitrary code
69
- # Source: https://numpy.org/doc/stable/reference/generated/numpy.load.html
70
- ndarray_deserialized = np.load(bytes_io, allow_pickle=False)
71
- return cast(NDArray, ndarray_deserialized)
72
-
73
-
74
- def _check_key(key: str) -> None:
75
- """Check if key is of expected type."""
76
- if not isinstance(key, str):
77
- raise TypeError(f"Key must be of type `str` but `{type(key)}` was passed.")
78
-
79
-
80
- def _check_value(value: Array) -> None:
81
- if not isinstance(value, Array):
82
- raise TypeError(
83
- f"Value must be of type `{Array}` but `{type(value)}` was passed."
84
- )
85
-
86
-
87
- class ParametersRecord(TypedDict[str, Array]):
88
- r"""Parameters record.
89
-
90
- A dataclass storing named Arrays in order. This means that it holds entries as an
91
- OrderedDict[str, Array]. ParametersRecord objects can be viewed as an equivalent to
92
- PyTorch's state_dict, but holding serialised tensors instead. A
93
- :code:`ParametersRecord` is one of the types of records that a
94
- `flwr.common.RecordSet <flwr.common.RecordSet.html#recordset>`_ supports and
95
- can therefore be used to construct :code:`common.Message` objects.
96
-
97
- Parameters
98
- ----------
99
- array_dict : Optional[OrderedDict[str, Array]]
100
- A dictionary that stores serialized array-like or tensor-like objects.
101
- keep_input : bool (default: False)
102
- A boolean indicating whether parameters should be deleted from the input
103
- dictionary immediately after adding them to the record. If False, the
104
- dictionary passed to `set_parameters()` will be empty once exiting from that
105
- function. This is the desired behaviour when working with very large
106
- models/tensors/arrays. However, if you plan to continue working with your
107
- parameters after adding it to the record, set this flag to True. When set
108
- to True, the data is duplicated in memory.
109
-
110
- Examples
111
- --------
112
- The usage of :code:`ParametersRecord` is envisioned for storing data arrays (e.g.
113
- parameters of a machine learning model). These first need to be serialized into
114
- a :code:`flwr.common.Array` data structure.
115
-
116
- Let's see some examples:
117
-
118
- >>> import numpy as np
119
- >>> from flwr.common import ParametersRecord
120
- >>> from flwr.common import array_from_numpy
121
- >>>
122
- >>> # Let's create a simple NumPy array
123
- >>> arr_np = np.random.randn(3, 3)
124
- >>>
125
- >>> # If we print it
126
- >>> array([[-1.84242409, -1.01539537, -0.46528405],
127
- >>> [ 0.32991896, 0.55540414, 0.44085534],
128
- >>> [-0.10758364, 1.97619858, -0.37120501]])
129
- >>>
130
- >>> # Let's create an Array out of it
131
- >>> arr = array_from_numpy(arr_np)
132
- >>>
133
- >>> # If we print it you'll see (note the binary data)
134
- >>> Array(dtype='float64', shape=[3,3], stype='numpy.ndarray', data=b'@\x99\x18...')
135
- >>>
136
- >>> # Adding it to a ParametersRecord:
137
- >>> p_record = ParametersRecord({"my_array": arr})
138
-
139
- Now that the NumPy array is embedded into a :code:`ParametersRecord` it could be
140
- sent if added as part of a :code:`common.Message` or it could be saved as a
141
- persistent state of a :code:`ClientApp` via its context. Regardless of the usecase,
142
- we will sooner or later want to recover the array in its original NumPy
143
- representation. For the example above, where the array was serialized using the
144
- built-in utility function, deserialization can be done as follows:
145
-
146
- >>> # Use the Array's built-in method
147
- >>> arr_np_d = arr.numpy()
148
- >>>
149
- >>> # If printed, it will show the exact same data as above:
150
- >>> array([[-1.84242409, -1.01539537, -0.46528405],
151
- >>> [ 0.32991896, 0.55540414, 0.44085534],
152
- >>> [-0.10758364, 1.97619858, -0.37120501]])
153
-
154
- If you need finer control on how your arrays are serialized and deserialized, you
155
- can construct :code:`Array` objects directly like this:
156
-
157
- >>> from flwr.common import Array
158
- >>> # Serialize your array and construct Array object
159
- >>> arr = Array(
160
- >>> data=ndarray.tobytes(),
161
- >>> dtype=str(ndarray.dtype),
162
- >>> stype="", # Could be used in a deserialization function
163
- >>> shape=list(ndarray.shape),
164
- >>> )
165
- >>>
166
- >>> # Then you can deserialize it like this
167
- >>> arr_np_d = np.frombuffer(
168
- >>> buffer=array.data,
169
- >>> dtype=array.dtype,
170
- >>> ).reshape(array.shape)
171
-
172
- Note that different arrays (e.g. from PyTorch, Tensorflow) might require different
173
- serialization mechanism. Howerver, they often support a conversion to NumPy,
174
- therefore allowing to use the same or similar steps as in the example above.
175
- """
176
-
177
- def __init__(
178
- self,
179
- array_dict: Optional[OrderedDict[str, Array]] = None,
180
- keep_input: bool = False,
181
- ) -> None:
182
- super().__init__(_check_key, _check_value)
183
- if array_dict:
184
- for k in list(array_dict.keys()):
185
- self[k] = array_dict[k]
186
- if not keep_input:
187
- del array_dict[k]
188
-
189
- def count_bytes(self) -> int:
190
- """Return number of Bytes stored in this object.
191
-
192
- Note that a small amount of Bytes might also be included in this counting that
193
- correspond to metadata of the serialized object (e.g. of NumPy array) needed for
194
- deseralization.
195
- """
196
- num_bytes = 0
197
-
198
- for k, v in self.items():
199
- num_bytes += len(v.data)
200
-
201
- # We also count the bytes footprint of the keys
202
- num_bytes += len(k)
203
-
204
- return num_bytes
@@ -1,202 +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
- """RecordSet."""
16
-
17
-
18
- from __future__ import annotations
19
-
20
- from dataclasses import dataclass
21
- from typing import cast
22
-
23
- from .configsrecord import ConfigsRecord
24
- from .metricsrecord import MetricsRecord
25
- from .parametersrecord import ParametersRecord
26
- from .typeddict import TypedDict
27
-
28
-
29
- @dataclass
30
- class RecordSetData:
31
- """Inner data container for the RecordSet class."""
32
-
33
- parameters_records: TypedDict[str, ParametersRecord]
34
- metrics_records: TypedDict[str, MetricsRecord]
35
- configs_records: TypedDict[str, ConfigsRecord]
36
-
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
48
- )
49
- self.configs_records = TypedDict[str, ConfigsRecord](
50
- self._check_fn_str, self._check_fn_configs
51
- )
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
-
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
-
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
- )
79
-
80
- def _check_fn_configs(self, record: ConfigsRecord) -> None:
81
- if not isinstance(record, ConfigsRecord):
82
- raise TypeError(
83
- f"Expected `{ConfigsRecord.__name__}`, but "
84
- f"received `{type(record).__name__}` for the value."
85
- )
86
-
87
-
88
- class RecordSet:
89
- """RecordSet stores groups of parameters, metrics and configs.
90
-
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.
96
-
97
- Parameters
98
- ----------
99
- parameters_records : Optional[Dict[str, ParametersRecord]]
100
- A dictionary of :code:`ParametersRecords` that can be used to record
101
- and communicate model parameters and high-dimensional arrays.
102
- metrics_records : Optional[Dict[str, MetricsRecord]]
103
- A dictionary of :code:`MetricsRecord` that can be used to record
104
- and communicate scalar-valued metrics that are the result of performing
105
- and action, for example, by a :code:`ClientApp`.
106
- configs_records : Optional[Dict[str, ConfigsRecord]]
107
- A dictionary of :code:`ConfigsRecord` that can be used to record
108
- and communicate configuration values to an entity (e.g. to a
109
- :code:`ClientApp`)
110
- for it to adjust how an action is performed.
111
-
112
- Examples
113
- --------
114
- A :code:`RecordSet` can hold three types of records, each designed
115
- with an specific purpose. What is common to all of them is that they
116
- are Python dictionaries designed to ensure that each key-value pair
117
- adheres to specified data types.
118
-
119
- Let's see an example.
120
-
121
- >>> from flwr.common import RecordSet
122
- >>> from flwr.common import ConfigsRecord, MetricsRecord, ParametersRecord
123
- >>>
124
- >>> # Let's begin with an empty record
125
- >>> my_recordset = RecordSet()
126
- >>>
127
- >>> # We can create a ConfigsRecord
128
- >>> c_record = ConfigsRecord({"lr": 0.1, "batch-size": 128})
129
- >>> # Adding it to the record_set would look like this
130
- >>> my_recordset.configs_records["my_config"] = c_record
131
- >>>
132
- >>> # We can create a MetricsRecord following a similar process
133
- >>> m_record = MetricsRecord({"accuracy": 0.93, "losses": [0.23, 0.1]})
134
- >>> # Adding it to the record_set would look like this
135
- >>> my_recordset.metrics_records["my_metrics"] = m_record
136
-
137
- Adding a :code:`ParametersRecord` follows the same steps as above but first,
138
- the array needs to be serialized and represented as a :code:`flwr.common.Array`.
139
- If the array is a :code:`NumPy` array, you can use the built-in utility function
140
- `array_from_numpy <flwr.common.array_from_numpy.html>`_. It is often possible to
141
- convert an array first to :code:`NumPy` and then use the aforementioned function.
142
-
143
- >>> from flwr.common import array_from_numpy
144
- >>> # Creating a ParametersRecord would look like this
145
- >>> arr_np = np.random.randn(3, 3)
146
- >>>
147
- >>> # You can use the built-in tool to serialize the array
148
- >>> arr = array_from_numpy(arr_np)
149
- >>>
150
- >>> # Finally, create the record
151
- >>> p_record = ParametersRecord({"my_array": arr})
152
- >>>
153
- >>> # Adding it to the record_set would look like this
154
- >>> my_recordset.parameters_records["my_parameters"] = p_record
155
-
156
- For additional examples on how to construct each of the records types shown
157
- above, please refer to the documentation for :code:`ConfigsRecord`,
158
- :code:`MetricsRecord` and :code:`ParametersRecord`.
159
- """
160
-
161
- def __init__(
162
- self,
163
- parameters_records: dict[str, ParametersRecord] | None = None,
164
- metrics_records: dict[str, MetricsRecord] | None = None,
165
- configs_records: dict[str, ConfigsRecord] | None = None,
166
- ) -> 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
173
-
174
- @property
175
- def parameters_records(self) -> TypedDict[str, ParametersRecord]:
176
- """Dictionary holding ParametersRecord instances."""
177
- data = cast(RecordSetData, self.__dict__["_data"])
178
- return data.parameters_records
179
-
180
- @property
181
- def metrics_records(self) -> TypedDict[str, MetricsRecord]:
182
- """Dictionary holding MetricsRecord instances."""
183
- data = cast(RecordSetData, self.__dict__["_data"])
184
- return data.metrics_records
185
-
186
- @property
187
- def configs_records(self) -> TypedDict[str, ConfigsRecord]:
188
- """Dictionary holding ConfigsRecord instances."""
189
- data = cast(RecordSetData, self.__dict__["_data"])
190
- return data.configs_records
191
-
192
- def __repr__(self) -> str:
193
- """Return a string representation of this instance."""
194
- 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__