flwr-nightly 1.9.0.dev20240426__py3-none-any.whl → 1.9.0.dev20240430__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/client/__init__.py +1 -1
- flwr/client/app.py +17 -93
- flwr/client/grpc_rere_client/client_interceptor.py +9 -1
- flwr/client/supernode/__init__.py +2 -0
- flwr/client/supernode/app.py +166 -4
- flwr/common/logger.py +26 -0
- flwr/common/message.py +72 -82
- flwr/common/record/recordset.py +5 -4
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +15 -0
- flwr/server/app.py +105 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +3 -1
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +174 -0
- flwr/simulation/app.py +16 -1
- flwr/simulation/run_simulation.py +3 -0
- {flwr_nightly-1.9.0.dev20240426.dist-info → flwr_nightly-1.9.0.dev20240430.dist-info}/METADATA +1 -1
- {flwr_nightly-1.9.0.dev20240426.dist-info → flwr_nightly-1.9.0.dev20240430.dist-info}/RECORD +19 -18
- {flwr_nightly-1.9.0.dev20240426.dist-info → flwr_nightly-1.9.0.dev20240430.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.9.0.dev20240426.dist-info → flwr_nightly-1.9.0.dev20240430.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.9.0.dev20240426.dist-info → flwr_nightly-1.9.0.dev20240430.dist-info}/entry_points.txt +0 -0
flwr/client/__init__.py
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"""Flower client."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from .app import run_client_app as run_client_app
|
|
19
18
|
from .app import start_client as start_client
|
|
20
19
|
from .app import start_numpy_client as start_numpy_client
|
|
21
20
|
from .client import Client as Client
|
|
22
21
|
from .client_app import ClientApp as ClientApp
|
|
23
22
|
from .numpy_client import NumPyClient as NumPyClient
|
|
23
|
+
from .supernode import run_client_app as run_client_app
|
|
24
24
|
from .supernode import run_supernode as run_supernode
|
|
25
25
|
from .typing import ClientFn as ClientFn
|
|
26
26
|
|
flwr/client/app.py
CHANGED
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower client app."""
|
|
16
16
|
|
|
17
|
-
import argparse
|
|
18
17
|
import sys
|
|
19
18
|
import time
|
|
20
19
|
from logging import DEBUG, ERROR, INFO, WARN
|
|
21
|
-
from pathlib import Path
|
|
22
20
|
from typing import Callable, ContextManager, Optional, Tuple, Type, Union
|
|
23
21
|
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
24
23
|
from grpc import RpcError
|
|
25
24
|
|
|
26
25
|
from flwr.client.client import Client
|
|
@@ -36,10 +35,8 @@ from flwr.common.constant import (
|
|
|
36
35
|
TRANSPORT_TYPES,
|
|
37
36
|
ErrorCode,
|
|
38
37
|
)
|
|
39
|
-
from flwr.common.exit_handlers import register_exit_handlers
|
|
40
38
|
from flwr.common.logger import log, warn_deprecated_feature
|
|
41
39
|
from flwr.common.message import Error
|
|
42
|
-
from flwr.common.object_ref import load_app, validate
|
|
43
40
|
from flwr.common.retry_invoker import RetryInvoker, exponential
|
|
44
41
|
|
|
45
42
|
from .grpc_client.connection import grpc_connection
|
|
@@ -47,94 +44,6 @@ from .grpc_rere_client.connection import grpc_request_response
|
|
|
47
44
|
from .message_handler.message_handler import handle_control_message
|
|
48
45
|
from .node_state import NodeState
|
|
49
46
|
from .numpy_client import NumPyClient
|
|
50
|
-
from .supernode.app import parse_args_run_client_app
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def run_client_app() -> None:
|
|
54
|
-
"""Run Flower client app."""
|
|
55
|
-
log(INFO, "Long-running Flower client starting")
|
|
56
|
-
|
|
57
|
-
event(EventType.RUN_CLIENT_APP_ENTER)
|
|
58
|
-
|
|
59
|
-
args = _parse_args_run_client_app().parse_args()
|
|
60
|
-
|
|
61
|
-
# Obtain certificates
|
|
62
|
-
if args.insecure:
|
|
63
|
-
if args.root_certificates is not None:
|
|
64
|
-
sys.exit(
|
|
65
|
-
"Conflicting options: The '--insecure' flag disables HTTPS, "
|
|
66
|
-
"but '--root-certificates' was also specified. Please remove "
|
|
67
|
-
"the '--root-certificates' option when running in insecure mode, "
|
|
68
|
-
"or omit '--insecure' to use HTTPS."
|
|
69
|
-
)
|
|
70
|
-
log(
|
|
71
|
-
WARN,
|
|
72
|
-
"Option `--insecure` was set. "
|
|
73
|
-
"Starting insecure HTTP client connected to %s.",
|
|
74
|
-
args.server,
|
|
75
|
-
)
|
|
76
|
-
root_certificates = None
|
|
77
|
-
else:
|
|
78
|
-
# Load the certificates if provided, or load the system certificates
|
|
79
|
-
cert_path = args.root_certificates
|
|
80
|
-
if cert_path is None:
|
|
81
|
-
root_certificates = None
|
|
82
|
-
else:
|
|
83
|
-
root_certificates = Path(cert_path).read_bytes()
|
|
84
|
-
log(
|
|
85
|
-
DEBUG,
|
|
86
|
-
"Starting secure HTTPS client connected to %s "
|
|
87
|
-
"with the following certificates: %s.",
|
|
88
|
-
args.server,
|
|
89
|
-
cert_path,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
log(
|
|
93
|
-
DEBUG,
|
|
94
|
-
"Flower will load ClientApp `%s`",
|
|
95
|
-
getattr(args, "client-app"),
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
client_app_dir = args.dir
|
|
99
|
-
if client_app_dir is not None:
|
|
100
|
-
sys.path.insert(0, client_app_dir)
|
|
101
|
-
|
|
102
|
-
app_ref: str = getattr(args, "client-app")
|
|
103
|
-
valid, error_msg = validate(app_ref)
|
|
104
|
-
if not valid and error_msg:
|
|
105
|
-
raise LoadClientAppError(error_msg) from None
|
|
106
|
-
|
|
107
|
-
def _load() -> ClientApp:
|
|
108
|
-
client_app = load_app(app_ref, LoadClientAppError)
|
|
109
|
-
|
|
110
|
-
if not isinstance(client_app, ClientApp):
|
|
111
|
-
raise LoadClientAppError(
|
|
112
|
-
f"Attribute {app_ref} is not of type {ClientApp}",
|
|
113
|
-
) from None
|
|
114
|
-
|
|
115
|
-
return client_app
|
|
116
|
-
|
|
117
|
-
_start_client_internal(
|
|
118
|
-
server_address=args.server,
|
|
119
|
-
load_client_app_fn=_load,
|
|
120
|
-
transport="rest" if args.rest else "grpc-rere",
|
|
121
|
-
root_certificates=root_certificates,
|
|
122
|
-
insecure=args.insecure,
|
|
123
|
-
max_retries=args.max_retries,
|
|
124
|
-
max_wait_time=args.max_wait_time,
|
|
125
|
-
)
|
|
126
|
-
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _parse_args_run_client_app() -> argparse.ArgumentParser:
|
|
130
|
-
"""Parse flower-client-app command line arguments."""
|
|
131
|
-
parser = argparse.ArgumentParser(
|
|
132
|
-
description="Start a Flower client app",
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
parse_args_run_client_app(parser=parser)
|
|
136
|
-
|
|
137
|
-
return parser
|
|
138
47
|
|
|
139
48
|
|
|
140
49
|
def _check_actionable_client(
|
|
@@ -165,6 +74,9 @@ def start_client(
|
|
|
165
74
|
root_certificates: Optional[Union[bytes, str]] = None,
|
|
166
75
|
insecure: Optional[bool] = None,
|
|
167
76
|
transport: Optional[str] = None,
|
|
77
|
+
authentication_keys: Optional[
|
|
78
|
+
Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
|
79
|
+
] = None,
|
|
168
80
|
max_retries: Optional[int] = None,
|
|
169
81
|
max_wait_time: Optional[float] = None,
|
|
170
82
|
) -> None:
|
|
@@ -249,6 +161,7 @@ def start_client(
|
|
|
249
161
|
root_certificates=root_certificates,
|
|
250
162
|
insecure=insecure,
|
|
251
163
|
transport=transport,
|
|
164
|
+
authentication_keys=authentication_keys,
|
|
252
165
|
max_retries=max_retries,
|
|
253
166
|
max_wait_time=max_wait_time,
|
|
254
167
|
)
|
|
@@ -269,6 +182,9 @@ def _start_client_internal(
|
|
|
269
182
|
root_certificates: Optional[Union[bytes, str]] = None,
|
|
270
183
|
insecure: Optional[bool] = None,
|
|
271
184
|
transport: Optional[str] = None,
|
|
185
|
+
authentication_keys: Optional[
|
|
186
|
+
Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
|
187
|
+
] = None,
|
|
272
188
|
max_retries: Optional[int] = None,
|
|
273
189
|
max_wait_time: Optional[float] = None,
|
|
274
190
|
) -> None:
|
|
@@ -393,6 +309,7 @@ def _start_client_internal(
|
|
|
393
309
|
retry_invoker,
|
|
394
310
|
grpc_max_message_length,
|
|
395
311
|
root_certificates,
|
|
312
|
+
authentication_keys,
|
|
396
313
|
) as conn:
|
|
397
314
|
# pylint: disable-next=W0612
|
|
398
315
|
receive, send, create_node, delete_node, get_run = conn
|
|
@@ -606,7 +523,14 @@ def start_numpy_client(
|
|
|
606
523
|
|
|
607
524
|
def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
608
525
|
Callable[
|
|
609
|
-
[
|
|
526
|
+
[
|
|
527
|
+
str,
|
|
528
|
+
bool,
|
|
529
|
+
RetryInvoker,
|
|
530
|
+
int,
|
|
531
|
+
Union[bytes, str, None],
|
|
532
|
+
Optional[Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
|
|
533
|
+
],
|
|
610
534
|
ContextManager[
|
|
611
535
|
Tuple[
|
|
612
536
|
Callable[[], Optional[Message]],
|
|
@@ -32,6 +32,7 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
|
32
32
|
CreateNodeRequest,
|
|
33
33
|
DeleteNodeRequest,
|
|
34
34
|
GetRunRequest,
|
|
35
|
+
PingRequest,
|
|
35
36
|
PullTaskInsRequest,
|
|
36
37
|
PushTaskResRequest,
|
|
37
38
|
)
|
|
@@ -45,6 +46,7 @@ Request = Union[
|
|
|
45
46
|
PullTaskInsRequest,
|
|
46
47
|
PushTaskResRequest,
|
|
47
48
|
GetRunRequest,
|
|
49
|
+
PingRequest,
|
|
48
50
|
]
|
|
49
51
|
|
|
50
52
|
|
|
@@ -115,7 +117,13 @@ class AuthenticateClientInterceptor(grpc.UnaryUnaryClientInterceptor): # type:
|
|
|
115
117
|
postprocess = True
|
|
116
118
|
elif isinstance(
|
|
117
119
|
request,
|
|
118
|
-
(
|
|
120
|
+
(
|
|
121
|
+
DeleteNodeRequest,
|
|
122
|
+
PullTaskInsRequest,
|
|
123
|
+
PushTaskResRequest,
|
|
124
|
+
GetRunRequest,
|
|
125
|
+
PingRequest,
|
|
126
|
+
),
|
|
119
127
|
):
|
|
120
128
|
if self.shared_secret is None:
|
|
121
129
|
raise RuntimeError("Failure to compute hmac")
|
flwr/client/supernode/app.py
CHANGED
|
@@ -15,11 +15,27 @@
|
|
|
15
15
|
"""Flower SuperNode."""
|
|
16
16
|
|
|
17
17
|
import argparse
|
|
18
|
-
|
|
18
|
+
import sys
|
|
19
|
+
from logging import DEBUG, INFO, WARN
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable, Optional, Tuple
|
|
19
22
|
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
24
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
25
|
+
load_ssh_private_key,
|
|
26
|
+
load_ssh_public_key,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
20
30
|
from flwr.common import EventType, event
|
|
21
31
|
from flwr.common.exit_handlers import register_exit_handlers
|
|
22
32
|
from flwr.common.logger import log
|
|
33
|
+
from flwr.common.object_ref import load_app, validate
|
|
34
|
+
from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|
|
35
|
+
ssh_types_to_elliptic_curve,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from ..app import _start_client_internal
|
|
23
39
|
|
|
24
40
|
|
|
25
41
|
def run_supernode() -> None:
|
|
@@ -41,6 +57,97 @@ def run_supernode() -> None:
|
|
|
41
57
|
)
|
|
42
58
|
|
|
43
59
|
|
|
60
|
+
def run_client_app() -> None:
|
|
61
|
+
"""Run Flower client app."""
|
|
62
|
+
log(INFO, "Long-running Flower client starting")
|
|
63
|
+
|
|
64
|
+
event(EventType.RUN_CLIENT_APP_ENTER)
|
|
65
|
+
|
|
66
|
+
args = _parse_args_run_client_app().parse_args()
|
|
67
|
+
|
|
68
|
+
root_certificates = _get_certificates(args)
|
|
69
|
+
log(
|
|
70
|
+
DEBUG,
|
|
71
|
+
"Flower will load ClientApp `%s`",
|
|
72
|
+
getattr(args, "client-app"),
|
|
73
|
+
)
|
|
74
|
+
load_fn = _get_load_client_app_fn(args)
|
|
75
|
+
authentication_keys = _try_setup_client_authentication(args)
|
|
76
|
+
|
|
77
|
+
_start_client_internal(
|
|
78
|
+
server_address=args.server,
|
|
79
|
+
load_client_app_fn=load_fn,
|
|
80
|
+
transport="rest" if args.rest else "grpc-rere",
|
|
81
|
+
root_certificates=root_certificates,
|
|
82
|
+
insecure=args.insecure,
|
|
83
|
+
authentication_keys=authentication_keys,
|
|
84
|
+
max_retries=args.max_retries,
|
|
85
|
+
max_wait_time=args.max_wait_time,
|
|
86
|
+
)
|
|
87
|
+
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
|
|
91
|
+
"""Load certificates if specified in args."""
|
|
92
|
+
# Obtain certificates
|
|
93
|
+
if args.insecure:
|
|
94
|
+
if args.root_certificates is not None:
|
|
95
|
+
sys.exit(
|
|
96
|
+
"Conflicting options: The '--insecure' flag disables HTTPS, "
|
|
97
|
+
"but '--root-certificates' was also specified. Please remove "
|
|
98
|
+
"the '--root-certificates' option when running in insecure mode, "
|
|
99
|
+
"or omit '--insecure' to use HTTPS."
|
|
100
|
+
)
|
|
101
|
+
log(
|
|
102
|
+
WARN,
|
|
103
|
+
"Option `--insecure` was set. "
|
|
104
|
+
"Starting insecure HTTP client connected to %s.",
|
|
105
|
+
args.server,
|
|
106
|
+
)
|
|
107
|
+
root_certificates = None
|
|
108
|
+
else:
|
|
109
|
+
# Load the certificates if provided, or load the system certificates
|
|
110
|
+
cert_path = args.root_certificates
|
|
111
|
+
if cert_path is None:
|
|
112
|
+
root_certificates = None
|
|
113
|
+
else:
|
|
114
|
+
root_certificates = Path(cert_path).read_bytes()
|
|
115
|
+
log(
|
|
116
|
+
DEBUG,
|
|
117
|
+
"Starting secure HTTPS client connected to %s "
|
|
118
|
+
"with the following certificates: %s.",
|
|
119
|
+
args.server,
|
|
120
|
+
cert_path,
|
|
121
|
+
)
|
|
122
|
+
return root_certificates
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_load_client_app_fn(
|
|
126
|
+
args: argparse.Namespace,
|
|
127
|
+
) -> Callable[[], ClientApp]:
|
|
128
|
+
"""Get the load_client_app_fn function."""
|
|
129
|
+
client_app_dir = args.dir
|
|
130
|
+
if client_app_dir is not None:
|
|
131
|
+
sys.path.insert(0, client_app_dir)
|
|
132
|
+
|
|
133
|
+
app_ref: str = getattr(args, "client-app")
|
|
134
|
+
valid, error_msg = validate(app_ref)
|
|
135
|
+
if not valid and error_msg:
|
|
136
|
+
raise LoadClientAppError(error_msg) from None
|
|
137
|
+
|
|
138
|
+
def _load() -> ClientApp:
|
|
139
|
+
client_app = load_app(app_ref, LoadClientAppError)
|
|
140
|
+
|
|
141
|
+
if not isinstance(client_app, ClientApp):
|
|
142
|
+
raise LoadClientAppError(
|
|
143
|
+
f"Attribute {app_ref} is not of type {ClientApp}",
|
|
144
|
+
) from None
|
|
145
|
+
|
|
146
|
+
return client_app
|
|
147
|
+
|
|
148
|
+
return _load
|
|
149
|
+
|
|
150
|
+
|
|
44
151
|
def _parse_args_run_supernode() -> argparse.ArgumentParser:
|
|
45
152
|
"""Parse flower-supernode command line arguments."""
|
|
46
153
|
parser = argparse.ArgumentParser(
|
|
@@ -57,17 +164,34 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
|
|
|
57
164
|
"If not provided, defaults to an empty string.",
|
|
58
165
|
)
|
|
59
166
|
_parse_args_common(parser)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--flwr-dir",
|
|
169
|
+
default=None,
|
|
170
|
+
help="""The path containing installed Flower Apps.
|
|
171
|
+
By default, this value isequal to:
|
|
172
|
+
|
|
173
|
+
- `$FLWR_HOME/` if `$FLWR_HOME` is defined
|
|
174
|
+
- `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
|
|
175
|
+
- `$HOME/.flwr/` in all other cases
|
|
176
|
+
""",
|
|
177
|
+
)
|
|
60
178
|
|
|
61
179
|
return parser
|
|
62
180
|
|
|
63
181
|
|
|
64
|
-
def
|
|
65
|
-
"""Parse command line arguments."""
|
|
182
|
+
def _parse_args_run_client_app() -> argparse.ArgumentParser:
|
|
183
|
+
"""Parse flower-client-app command line arguments."""
|
|
184
|
+
parser = argparse.ArgumentParser(
|
|
185
|
+
description="Start a Flower client app",
|
|
186
|
+
)
|
|
187
|
+
|
|
66
188
|
parser.add_argument(
|
|
67
189
|
"client-app",
|
|
68
190
|
help="For example: `client:app` or `project.package.module:wrapper.app`",
|
|
69
191
|
)
|
|
70
|
-
_parse_args_common(parser)
|
|
192
|
+
_parse_args_common(parser=parser)
|
|
193
|
+
|
|
194
|
+
return parser
|
|
71
195
|
|
|
72
196
|
|
|
73
197
|
def _parse_args_common(parser: argparse.ArgumentParser) -> None:
|
|
@@ -117,3 +241,41 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
|
|
|
117
241
|
"app from there."
|
|
118
242
|
" Default: current working directory.",
|
|
119
243
|
)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--authentication-keys",
|
|
246
|
+
nargs=2,
|
|
247
|
+
metavar=("CLIENT_PRIVATE_KEY", "CLIENT_PUBLIC_KEY"),
|
|
248
|
+
type=str,
|
|
249
|
+
help="Provide two file paths: (1) the client's private "
|
|
250
|
+
"key file, and (2) the client's public key file.",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _try_setup_client_authentication(
|
|
255
|
+
args: argparse.Namespace,
|
|
256
|
+
) -> Optional[Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]:
|
|
257
|
+
if not args.authentication_keys:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
ssh_private_key = load_ssh_private_key(
|
|
261
|
+
Path(args.authentication_keys[0]).read_bytes(),
|
|
262
|
+
None,
|
|
263
|
+
)
|
|
264
|
+
ssh_public_key = load_ssh_public_key(Path(args.authentication_keys[1]).read_bytes())
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
client_private_key, client_public_key = ssh_types_to_elliptic_curve(
|
|
268
|
+
ssh_private_key, ssh_public_key
|
|
269
|
+
)
|
|
270
|
+
except TypeError:
|
|
271
|
+
sys.exit(
|
|
272
|
+
"The file paths provided could not be read as a private and public "
|
|
273
|
+
"key pair. Client authentication requires an elliptic curve public and "
|
|
274
|
+
"private key pair. Please provide the file paths containing elliptic "
|
|
275
|
+
"curve private and public keys to '--authentication-keys'."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
client_private_key,
|
|
280
|
+
client_public_key,
|
|
281
|
+
)
|
flwr/common/logger.py
CHANGED
|
@@ -188,3 +188,29 @@ def warn_deprecated_feature(name: str) -> None:
|
|
|
188
188
|
""",
|
|
189
189
|
name,
|
|
190
190
|
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def set_logger_propagation(
|
|
194
|
+
child_logger: logging.Logger, value: bool = True
|
|
195
|
+
) -> logging.Logger:
|
|
196
|
+
"""Set the logger propagation attribute.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
child_logger : logging.Logger
|
|
201
|
+
Child logger object
|
|
202
|
+
value : bool
|
|
203
|
+
Boolean setting for propagation. If True, both parent and child logger
|
|
204
|
+
display messages. Otherwise, only the child logger displays a message.
|
|
205
|
+
This False setting prevents duplicate logs in Colab notebooks.
|
|
206
|
+
Reference: https://stackoverflow.com/a/19561320
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
logging.Logger
|
|
211
|
+
Child logger object with updated propagation setting
|
|
212
|
+
"""
|
|
213
|
+
child_logger.propagate = value
|
|
214
|
+
if not child_logger.propagate:
|
|
215
|
+
child_logger.log(logging.DEBUG, "Logger propagate set to False")
|
|
216
|
+
return child_logger
|