wandb 0.18.6__py3-none-win_amd64.whl → 0.19.0__py3-none-win_amd64.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. package_readme.md +8 -0
  2. wandb/__init__.py +5 -7
  3. wandb/__init__.pyi +51 -30
  4. wandb/analytics/sentry.py +4 -10
  5. wandb/apis/importers/internals/internal.py +6 -6
  6. wandb/apis/importers/internals/protocols.py +11 -7
  7. wandb/apis/public/api.py +5 -1
  8. wandb/apis/public/jobs.py +1 -7
  9. wandb/apis/public/reports.py +6 -17
  10. wandb/apis/public/runs.py +12 -10
  11. wandb/bin/gpu_stats.exe +0 -0
  12. wandb/bin/wandb-core +0 -0
  13. wandb/cli/cli.py +9 -45
  14. wandb/env.py +3 -5
  15. wandb/errors/links.py +1 -1
  16. wandb/errors/term.py +1 -6
  17. wandb/filesync/dir_watcher.py +3 -3
  18. wandb/filesync/step_upload.py +2 -5
  19. wandb/integration/fastai/__init__.py +1 -6
  20. wandb/integration/gym/__init__.py +1 -7
  21. wandb/integration/keras/callbacks/metrics_logger.py +1 -8
  22. wandb/integration/keras/callbacks/model_checkpoint.py +1 -8
  23. wandb/integration/keras/keras.py +3 -5
  24. wandb/integration/lightgbm/__init__.py +1 -1
  25. wandb/integration/sb3/sb3.py +1 -7
  26. wandb/integration/sklearn/utils.py +1 -1
  27. wandb/integration/tensorboard/log.py +1 -2
  28. wandb/integration/torch/wandb_torch.py +1 -1
  29. wandb/integration/ultralytics/bbox_utils.py +9 -2
  30. wandb/jupyter.py +4 -4
  31. wandb/proto/v3/wandb_internal_pb2.py +31 -31
  32. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  33. wandb/proto/v3/wandb_telemetry_pb2.py +4 -4
  34. wandb/proto/v4/wandb_internal_pb2.py +31 -31
  35. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  36. wandb/proto/v4/wandb_telemetry_pb2.py +4 -4
  37. wandb/proto/v5/wandb_internal_pb2.py +31 -31
  38. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  39. wandb/proto/v5/wandb_telemetry_pb2.py +4 -4
  40. wandb/proto/wandb_deprecated.py +3 -11
  41. wandb/proto/wandb_generate_deprecated.py +3 -7
  42. wandb/sdk/artifacts/artifact.py +3 -11
  43. wandb/sdk/artifacts/artifact_file_cache.py +2 -5
  44. wandb/sdk/artifacts/artifact_saver.py +2 -6
  45. wandb/sdk/artifacts/storage_handlers/gcs_handler.py +2 -4
  46. wandb/sdk/artifacts/storage_handlers/local_file_handler.py +2 -4
  47. wandb/sdk/artifacts/storage_handlers/s3_handler.py +2 -4
  48. wandb/sdk/backend/backend.py +1 -1
  49. wandb/sdk/data_types/base_types/wb_value.py +20 -10
  50. wandb/sdk/data_types/histogram.py +1 -3
  51. wandb/sdk/data_types/object_3d.py +2 -6
  52. wandb/sdk/data_types/table.py +1 -1
  53. wandb/sdk/data_types/utils.py +1 -2
  54. wandb/sdk/data_types/video.py +15 -4
  55. wandb/sdk/integration_utils/auto_logging.py +1 -8
  56. wandb/sdk/interface/interface.py +12 -5
  57. wandb/sdk/interface/interface_queue.py +0 -6
  58. wandb/sdk/interface/interface_shared.py +9 -0
  59. wandb/sdk/interface/router.py +1 -2
  60. wandb/sdk/interface/router_queue.py +0 -3
  61. wandb/sdk/interface/router_relay.py +0 -2
  62. wandb/sdk/internal/file_stream.py +1 -4
  63. wandb/sdk/internal/flow_control.py +1 -1
  64. wandb/sdk/internal/handler.py +8 -5
  65. wandb/sdk/internal/internal.py +3 -17
  66. wandb/sdk/internal/internal_api.py +3 -10
  67. wandb/sdk/internal/internal_util.py +0 -3
  68. wandb/sdk/internal/job_builder.py +20 -12
  69. wandb/sdk/internal/progress.py +1 -5
  70. wandb/sdk/internal/sender.py +9 -15
  71. wandb/sdk/internal/settings_static.py +4 -10
  72. wandb/sdk/internal/system/assets/cpu.py +2 -2
  73. wandb/sdk/internal/system/assets/disk.py +3 -3
  74. wandb/sdk/internal/system/assets/gpu.py +7 -7
  75. wandb/sdk/internal/system/assets/gpu_amd.py +1 -7
  76. wandb/sdk/internal/system/assets/interfaces.py +11 -13
  77. wandb/sdk/internal/system/assets/ipu.py +1 -1
  78. wandb/sdk/internal/system/assets/memory.py +2 -2
  79. wandb/sdk/internal/system/assets/open_metrics.py +2 -8
  80. wandb/sdk/internal/system/assets/trainium.py +3 -9
  81. wandb/sdk/internal/system/system_info.py +14 -13
  82. wandb/sdk/internal/system/system_monitor.py +5 -12
  83. wandb/sdk/internal/tb_watcher.py +1 -1
  84. wandb/sdk/internal/writer.py +2 -4
  85. wandb/sdk/launch/__init__.py +2 -1
  86. wandb/sdk/launch/agent/run_queue_item_file_saver.py +1 -7
  87. wandb/sdk/launch/create_job.py +2 -3
  88. wandb/sdk/launch/runner/abstract.py +1 -6
  89. wandb/sdk/launch/runner/kubernetes_monitor.py +2 -4
  90. wandb/sdk/lib/apikey.py +2 -6
  91. wandb/sdk/lib/fsm.py +12 -6
  92. wandb/sdk/lib/ipython.py +1 -6
  93. wandb/sdk/lib/module.py +0 -3
  94. wandb/sdk/lib/progress.py +2 -3
  95. wandb/sdk/lib/run_moment.py +1 -7
  96. wandb/sdk/lib/server.py +10 -24
  97. wandb/sdk/lib/sock_client.py +0 -5
  98. wandb/sdk/service/server.py +3 -12
  99. wandb/sdk/service/server_sock.py +0 -2
  100. wandb/sdk/service/service.py +5 -5
  101. wandb/sdk/wandb_init.py +215 -166
  102. wandb/sdk/wandb_login.py +17 -27
  103. wandb/sdk/wandb_run.py +129 -161
  104. wandb/sdk/wandb_settings.py +978 -1760
  105. wandb/sdk/wandb_setup.py +87 -94
  106. wandb/sdk/wandb_watch.py +1 -1
  107. wandb/sync/sync.py +1 -2
  108. wandb/util.py +7 -40
  109. wandb/wandb_controller.py +10 -12
  110. {wandb-0.18.6.dist-info → wandb-0.19.0.dist-info}/METADATA +14 -4
  111. {wandb-0.18.6.dist-info → wandb-0.19.0.dist-info}/RECORD +114 -120
  112. {wandb-0.18.6.dist-info → wandb-0.19.0.dist-info}/WHEEL +1 -1
  113. wandb/integration/magic.py +0 -556
  114. wandb/magic.py +0 -3
  115. wandb/sdk/lib/_settings_toposort_generate.py +0 -159
  116. wandb/sdk/lib/_settings_toposort_generated.py +0 -250
  117. wandb/sdk/lib/reporting.py +0 -99
  118. wandb/sdk/lib/tracelog.py +0 -255
  119. {wandb-0.18.6.dist-info → wandb-0.19.0.dist-info}/entry_points.txt +0 -0
  120. {wandb-0.18.6.dist-info → wandb-0.19.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
- import collections.abc
1
+ from __future__ import annotations
2
+
2
3
  import configparser
3
- import enum
4
4
  import getpass
5
5
  import json
6
6
  import logging
@@ -12,1026 +12,517 @@ import shutil
12
12
  import socket
13
13
  import sys
14
14
  import tempfile
15
- import time
16
- from dataclasses import dataclass
17
15
  from datetime import datetime
18
- from distutils.util import strtobool
19
- from functools import reduce
20
- from typing import (
21
- Any,
22
- Callable,
23
- Dict,
24
- FrozenSet,
25
- ItemsView,
26
- Iterable,
27
- Mapping,
28
- Optional,
29
- Sequence,
30
- Set,
31
- Tuple,
32
- Union,
33
- no_type_check,
34
- )
35
- from urllib.parse import quote, unquote, urlencode, urlparse, urlsplit
16
+ from typing import Any, Literal, Sequence
17
+ from urllib.parse import quote, unquote, urlencode
18
+
19
+ if sys.version_info >= (3, 11):
20
+ from typing import Self
21
+ else:
22
+ from typing_extensions import Self
36
23
 
37
24
  from google.protobuf.wrappers_pb2 import BoolValue, DoubleValue, Int32Value, StringValue
25
+ from pydantic import (
26
+ BaseModel,
27
+ ConfigDict,
28
+ Field,
29
+ computed_field,
30
+ field_validator,
31
+ model_validator,
32
+ )
33
+ from pydantic_core import SchemaValidator, core_schema
38
34
 
39
35
  import wandb
40
- import wandb.env
41
- from wandb import util
36
+ from wandb import env, termwarn, util
42
37
  from wandb.apis.internal import Api
43
38
  from wandb.errors import UsageError
44
39
  from wandb.proto import wandb_settings_pb2
45
- from wandb.sdk.internal.system.env_probe_helpers import is_aws_lambda
46
- from wandb.sdk.lib import credentials, filesystem
47
- from wandb.sdk.lib._settings_toposort_generated import SETTINGS_TOPOLOGICALLY_SORTED
48
- from wandb.sdk.lib.run_moment import RunMoment
49
- from wandb.sdk.wandb_setup import _EarlyLogger
50
40
 
51
- from .lib import apikey, ipython
41
+ from .lib import apikey, credentials, filesystem, ipython
52
42
  from .lib.gitlib import GitRepo
43
+ from .lib.run_moment import RunMoment
53
44
  from .lib.runid import generate_id
54
45
 
55
- if sys.version_info >= (3, 8):
56
- from typing import get_args, get_origin, get_type_hints
57
- else:
58
- from typing_extensions import get_args, get_origin, get_type_hints
59
-
60
46
 
61
- class SettingsPreprocessingError(UsageError):
62
- """Raised when the value supplied to a wandb.Settings() setting does not pass preprocessing."""
47
+ def _path_convert(*args: str) -> str:
48
+ """Join path and apply os.path.expanduser to it."""
49
+ return os.path.expanduser(os.path.join(*args))
63
50
 
64
51
 
65
- class SettingsValidationError(UsageError):
66
- """Raised when the value supplied to a wandb.Settings() setting does not pass validation."""
67
-
68
-
69
- class SettingsUnexpectedArgsError(UsageError):
70
- """Raised when unexpected arguments are passed to wandb.Settings()."""
71
-
72
-
73
- def _get_wandb_dir(root_dir: str) -> str:
74
- """Get the full path to the wandb directory.
75
-
76
- The setting exposed to users as `dir=` or `WANDB_DIR` is the `root_dir`.
77
- We add the `__stage_dir__` to it to get the full `wandb_dir`
78
- """
79
- # We use the hidden version if it already exists, otherwise non-hidden.
80
- if os.path.exists(os.path.join(root_dir, ".wandb")):
81
- __stage_dir__ = ".wandb" + os.sep
82
- else:
83
- __stage_dir__ = "wandb" + os.sep
84
-
85
- path = os.path.join(root_dir, __stage_dir__)
86
- if not os.access(root_dir or ".", os.W_OK):
87
- wandb.termwarn(
88
- f"Path {path} wasn't writable, using system temp directory.",
89
- repeat=False,
90
- )
91
- path = os.path.join(tempfile.gettempdir(), __stage_dir__ or ("wandb" + os.sep))
92
-
93
- return os.path.expanduser(path)
94
-
95
-
96
- def _str_as_bool(val: Union[str, bool]) -> bool:
97
- """Parse a string as a bool."""
98
- if isinstance(val, bool):
99
- return val
100
- try:
101
- ret_val = bool(strtobool(str(val)))
102
- return ret_val
103
- except (AttributeError, ValueError):
104
- pass
105
-
106
- raise UsageError(f"Could not parse value {val} as a bool.")
107
-
108
-
109
- def _str_as_json(val: Union[str, Dict[str, Any]]) -> Any:
110
- """Parse a string as a json object."""
111
- if not isinstance(val, str):
112
- return val
113
- try:
114
- return json.loads(val)
115
- except (AttributeError, ValueError):
116
- pass
117
-
118
- raise UsageError(f"Could not parse value {val} as JSON.")
119
-
120
-
121
- def _str_as_tuple(val: Union[str, Sequence[str]]) -> Tuple[str, ...]:
122
- """Parse a (potentially comma-separated) string as a tuple."""
123
- if isinstance(val, str):
124
- return tuple(val.split(","))
125
- return tuple(val)
126
-
127
-
128
- def _datetime_as_str(val: Union[datetime, str]) -> str:
129
- """Parse a datetime object as a string."""
130
- if isinstance(val, datetime):
131
- return datetime.strftime(val, "%Y%m%d_%H%M%S")
132
- return val
133
-
134
-
135
- def _redact_dict(
136
- d: Dict[str, Any],
137
- unsafe_keys: Union[Set[str], FrozenSet[str]] = frozenset({"api_key"}),
138
- redact_str: str = "***REDACTED***",
139
- ) -> Dict[str, Any]:
140
- """Redact a dict of unsafe values specified by their key."""
141
- if not d or unsafe_keys.isdisjoint(d):
142
- return d
143
- safe_dict = d.copy()
144
- safe_dict.update({k: redact_str for k in unsafe_keys.intersection(d)})
145
- return safe_dict
146
-
147
-
148
- def _get_program() -> Optional[str]:
149
- program = os.getenv(wandb.env.PROGRAM)
150
- if program is not None:
151
- return program
152
- try:
153
- import __main__
154
-
155
- if __main__.__spec__ is None:
156
- return __main__.__file__
157
- # likely run as `python -m ...`
158
- return f"-m {__main__.__spec__.name}"
159
- except (ImportError, AttributeError):
160
- return None
161
-
162
-
163
- def _runmoment_preprocessor(val: Any) -> Optional[RunMoment]:
164
- if isinstance(val, RunMoment) or val is None:
165
- return val
166
- elif isinstance(val, str):
167
- return RunMoment.from_uri(val)
168
- raise UsageError(f"Could not parse value {val} as a RunMoment.")
169
-
170
-
171
- def _get_program_relpath(
172
- program: str, root: Optional[str] = None, _logger: Optional[_EarlyLogger] = None
173
- ) -> Optional[str]:
174
- if not program:
175
- if _logger is not None:
176
- _logger.warning("Empty program passed to get_program_relpath")
177
- return None
178
-
179
- root = root or os.getcwd()
180
- if not root:
181
- return None
52
+ class Settings(BaseModel, validate_assignment=True):
53
+ """Settings for the W&B SDK."""
182
54
 
183
- full_path_to_program = os.path.join(
184
- root, os.path.relpath(os.getcwd(), root), program
55
+ # Pydantic configuration.
56
+ model_config = ConfigDict(
57
+ extra="forbid", # throw an error if extra fields are provided
58
+ # validate_default=True, # validate default values
185
59
  )
186
- if os.path.exists(full_path_to_program):
187
- relative_path = os.path.relpath(full_path_to_program, start=root)
188
- if "../" in relative_path:
189
- if _logger is not None:
190
- _logger.warning(f"Could not save program above cwd: {program}")
191
- return None
192
- return relative_path
193
60
 
194
- if _logger is not None:
195
- _logger.warning(f"Could not find program at {program}")
196
- return None
197
-
198
-
199
- def is_instance_recursive(obj: Any, type_hint: Any) -> bool: # noqa: C901
200
- if type_hint is Any:
201
- return True
202
-
203
- origin = get_origin(type_hint)
204
- args = get_args(type_hint)
205
-
206
- if origin is None:
207
- return isinstance(obj, type_hint)
208
-
209
- if origin is Union:
210
- return any(is_instance_recursive(obj, arg) for arg in args)
211
-
212
- if issubclass(origin, collections.abc.Mapping):
213
- if not isinstance(obj, collections.abc.Mapping):
214
- return False
215
- key_type, value_type = args
216
-
217
- for key, value in obj.items():
218
- if not is_instance_recursive(key, key_type) or not is_instance_recursive(
219
- value, value_type
220
- ):
221
- return False
222
-
223
- return True
224
-
225
- if issubclass(origin, collections.abc.Sequence):
226
- if not isinstance(obj, collections.abc.Sequence) or isinstance(
227
- obj, (str, bytes, bytearray)
228
- ):
229
- return False
230
-
231
- if len(args) == 1 and args[0] != ...:
232
- (item_type,) = args
233
- for item in obj:
234
- if not is_instance_recursive(item, item_type):
235
- return False
236
- elif len(args) == 2 and args[-1] == ...:
237
- item_type = args[0]
238
- for item in obj:
239
- if not is_instance_recursive(item, item_type):
240
- return False
241
- elif len(args) == len(obj):
242
- for item, item_type in zip(obj, args):
243
- if not is_instance_recursive(item, item_type):
244
- return False
245
- else:
246
- return False
247
-
248
- return True
249
-
250
- if issubclass(origin, collections.abc.Set):
251
- if not isinstance(obj, collections.abc.Set):
252
- return False
253
-
254
- (item_type,) = args
255
- for item in obj:
256
- if not is_instance_recursive(item, item_type):
257
- return False
258
-
259
- return True
260
-
261
- return False
262
-
263
-
264
- @enum.unique
265
- class Source(enum.IntEnum):
266
- OVERRIDE: int = 0
267
- BASE: int = 1 # todo: audit this
268
- ORG: int = 2
269
- ENTITY: int = 3
270
- PROJECT: int = 4
271
- USER: int = 5
272
- SYSTEM: int = 6
273
- WORKSPACE: int = 7
274
- ENV: int = 8
275
- SETUP: int = 9
276
- LOGIN: int = 10
277
- INIT: int = 11
278
- SETTINGS: int = 12
279
- ARGS: int = 13
280
- RUN: int = 14
281
-
282
-
283
- ConsoleValue = {
284
- "auto",
285
- "off",
286
- "wrap",
287
- "redirect",
288
- # internal console states
289
- "wrap_raw",
290
- "wrap_emu",
291
- }
292
-
293
-
294
- @dataclass()
295
- class SettingsData:
296
- """Settings for the W&B SDK."""
297
-
298
- _args: Sequence[str]
299
- _aws_lambda: bool
300
- _cli_only_mode: bool # Avoid running any code specific for runs
301
- _code_path_local: str
302
- _colab: bool
303
- # _config_dict: Config
304
- _cuda: str
305
- _disable_meta: bool # Do not collect system metadata
306
- _disable_service: (
307
- bool # Disable wandb-service, spin up internal process the old way
61
+ # Public settings.
62
+
63
+ # Flag to allow table artifacts to be synced in offline mode.
64
+ #
65
+ # To revert to the old behavior, set this to False.
66
+ allow_offline_artifacts: bool = True
67
+ allow_val_change: bool = False
68
+ anonymous: Literal["allow", "must", "never"] | None = None
69
+ # The W&B API key.
70
+ api_key: str | None = None
71
+ azure_account_url_to_access_key: dict[str, str] | None = None
72
+ # The URL of the W&B backend, used for GraphQL and filestream operations.
73
+ base_url: str = "https://api.wandb.ai"
74
+ code_dir: str | None = None
75
+ config_paths: Sequence[str] | None = None
76
+ # The type of console capture to be applied. Possible values are:
77
+ # "auto" - Automatically selects the console capture method based on the
78
+ # system environment and settings.
79
+ #
80
+ # "off" - Disables console capture.
81
+ #
82
+ # "redirect" - Redirects low-level file descriptors for capturing output.
83
+ #
84
+ # "wrap" - Overrides the write methods of sys.stdout/sys.stderr. Will be
85
+ # mapped to either "wrap_raw" or "wrap_emu" based on the state of the system.
86
+ #
87
+ # "wrap_raw" - Same as "wrap" but captures raw output directly instead of
88
+ # through an emulator.
89
+ #
90
+ # "wrap_emu" - Same as "wrap" but captures output through an emulator.
91
+ console: Literal["auto", "off", "wrap", "redirect", "wrap_raw", "wrap_emu"] = Field(
92
+ default="auto",
93
+ validate_default=True,
308
94
  )
309
- _disable_setproctitle: bool # Do not use setproctitle on internal process
310
- _disable_stats: bool # Do not collect system metrics
311
- _disable_update_check: bool # Disable version check
312
- _disable_viewer: bool # Prevent early viewer query
313
- _disable_machine_info: bool # Disable automatic machine info collection
314
- _executable: str
315
- _extra_http_headers: Mapping[str, str]
316
- _file_stream_max_bytes: int # max size for filestream requests in core
317
- _file_stream_transmit_interval: float # tx interval for filestream requests in core
318
- # file stream retry client configuration
319
- _file_stream_retry_max: int # max number of retries
320
- _file_stream_retry_wait_min_seconds: float # min wait time between retries
321
- _file_stream_retry_wait_max_seconds: float # max wait time between retries
322
- _file_stream_timeout_seconds: float # timeout for individual HTTP requests
323
- # file transfer retry client configuration
324
- _file_transfer_retry_max: int
325
- _file_transfer_retry_wait_min_seconds: float
326
- _file_transfer_retry_wait_max_seconds: float
327
- _file_transfer_timeout_seconds: float
328
- _flow_control_custom: bool
329
- _flow_control_disabled: bool
330
- # graphql retry client configuration
331
- _graphql_retry_max: int
332
- _graphql_retry_wait_min_seconds: float
333
- _graphql_retry_wait_max_seconds: float
334
- _graphql_timeout_seconds: float
335
- _internal_check_process: float
336
- _internal_queue_timeout: float
337
- _ipython: bool
338
- _jupyter: bool
339
- _jupyter_name: str
340
- _jupyter_path: str
341
- _jupyter_root: str
342
- _kaggle: bool
343
- _live_policy_rate_limit: int
344
- _live_policy_wait_time: int
345
- _log_level: int
346
- _network_buffer: int
347
- _noop: bool
348
- _notebook: bool
349
- _offline: bool
350
- _sync: bool
351
- _os: str
352
- _platform: str
353
- _proxies: Mapping[
354
- str, str
355
- ] # custom proxy servers for the requests to W&B [scheme -> url]
356
- _python: str
357
- _runqueue_item_id: str
358
- _require_legacy_service: bool
359
- _save_requirements: bool
360
- _service_transport: str
361
- _service_wait: float
362
- _shared: bool
363
- _start_datetime: str
364
- _start_time: float
365
- _stats_pid: int # (internal) base pid for system stats
366
- _stats_sampling_interval: float # sampling interval for system stats
367
- _stats_sample_rate_seconds: float # badly-named sampling interval, deprecated
368
- _stats_samples_to_average: (
369
- int # number of samples to average before reporting, deprecated
95
+ # Whether to produce multipart console log files.
96
+ console_multipart: bool = False
97
+ # Path to file for writing temporary access tokens.
98
+ credentials_file: str = Field(
99
+ default_factory=lambda: str(credentials.DEFAULT_WANDB_CREDENTIALS_FILE)
370
100
  )
371
- _stats_join_assets: (
372
- bool # join metrics from different assets before sending to backend
101
+ # Whether to disable code saving.
102
+ disable_code: bool = False
103
+ # Whether to disable capturing the git state.
104
+ disable_git: bool = False
105
+ # Whether to disable the creation of a job artifact for W&B Launch.
106
+ disable_job_creation: bool = True
107
+ # The Docker image used to execute the script.
108
+ docker: str | None = None
109
+ # The email address of the user.
110
+ email: str | None = None
111
+ # The W&B entity, like a user or a team.
112
+ entity: str | None = None
113
+ force: bool = False
114
+ fork_from: RunMoment | None = None
115
+ git_commit: str | None = None
116
+ git_remote: str = "origin"
117
+ git_remote_url: str | None = None
118
+ git_root: str | None = None
119
+ heartbeat_seconds: int = 30
120
+ host: str | None = None
121
+ # The custom proxy servers for http requests to W&B.
122
+ http_proxy: str | None = None
123
+ # The custom proxy servers for https requests to W&B.
124
+ https_proxy: str | None = None
125
+ # Path to file containing an identity token (JWT) for authentication.
126
+ identity_token_file: str | None = None
127
+ # Unix glob patterns relative to `files_dir` to not upload.
128
+ ignore_globs: tuple[str, ...] = ()
129
+ init_timeout: float = 90.0
130
+ job_name: str | None = None
131
+ job_source: Literal["repo", "artifact", "image"] | None = None
132
+ label_disable: bool = False
133
+ launch: bool = False
134
+ launch_config_path: str | None = None
135
+ login_timeout: float | None = None
136
+ mode: Literal["online", "offline", "dryrun", "disabled", "run", "shared"] = Field(
137
+ default="online",
138
+ validate_default=True,
373
139
  )
374
- _stats_neuron_monitor_config_path: (
375
- str # path to place config file for neuron-monitor (AWS Trainium)
140
+ notebook_name: str | None = None
141
+ # Path to the script that created the run, if available.
142
+ program: str | None = None
143
+ # The absolute path from the root repository directory to the script that
144
+ # created the run.
145
+ #
146
+ # Root repository directory is defined as the directory containing the
147
+ # .git directory, if it exists. Otherwise, it's the current working directory.
148
+ program_abspath: str | None = None
149
+ program_relpath: str | None = None
150
+ # The W&B project ID.
151
+ project: str | None = None
152
+ quiet: bool = False
153
+ reinit: bool = False
154
+ relogin: bool = False
155
+ # Specifies the resume behavior for the run. The available options are:
156
+ #
157
+ # "must": Resumes from an existing run with the same ID. If no such run exists,
158
+ # it will result in failure.
159
+ #
160
+ # "allow": Attempts to resume from an existing run with the same ID. If none is
161
+ # found, a new run will be created.
162
+ #
163
+ # "never": Always starts a new run. If a run with the same ID already exists,
164
+ # it will result in failure.
165
+ #
166
+ # "auto": Automatically resumes from the most recent failed run on the same
167
+ # machine.
168
+ resume: Literal["allow", "must", "never", "auto"] | None = None
169
+ resume_from: RunMoment | None = None
170
+ # Indication from the server about the state of the run.
171
+ #
172
+ # This is different from resume, a user provided flag.
173
+ resumed: bool = False
174
+ # The root directory that will be used to derive other paths,
175
+ # such as the wandb directory, and the run directory.
176
+ root_dir: str = Field(default_factory=lambda: os.path.abspath(os.getcwd()))
177
+ run_group: str | None = None
178
+ # The ID of the run.
179
+ run_id: str | None = None
180
+ run_job_type: str | None = None
181
+ run_name: str | None = None
182
+ run_notes: str | None = None
183
+ run_tags: tuple[str, ...] | None = None
184
+ sagemaker_disable: bool = False
185
+ save_code: bool | None = None
186
+ settings_system: str = Field(
187
+ default_factory=lambda: _path_convert(
188
+ os.path.join("~", ".config", "wandb", "settings")
189
+ )
190
+ )
191
+ show_colors: bool | None = None
192
+ show_emoji: bool | None = None
193
+ show_errors: bool = True
194
+ show_info: bool = True
195
+ show_warnings: bool = True
196
+ silent: bool = False
197
+ start_method: str | None = None
198
+ strict: bool | None = None
199
+ summary_timeout: int = 60
200
+ summary_warnings: int = 5 # TODO: kill this with fire
201
+ sweep_id: str | None = None
202
+ sweep_param_path: str | None = None
203
+ symlink: bool = Field(
204
+ default_factory=lambda: False if platform.system() == "Windows" else True
376
205
  )
377
- _stats_open_metrics_endpoints: Mapping[str, str] # open metrics endpoint names/urls
378
- # open metrics filters in one of the two formats:
206
+ sync_tensorboard: bool | None = None
207
+ table_raise_on_max_row_limit_exceeded: bool = False
208
+ username: str | None = None
209
+
210
+ # Internal settings.
211
+ #
212
+ # These are typically not meant to be set by the user and should not be considered
213
+ # a part of the public API as they may change or be removed in future versions.
214
+
215
+ # CLI mode.
216
+ x_cli_only_mode: bool = False
217
+ # Disable the collection of system metadata.
218
+ x_disable_meta: bool = False
219
+ # Pre-wandb-core, this setting was used to disable the (now legacy) wandb service.
220
+ #
221
+ # TODO: this is deprecated and will be removed in future versions.
222
+ x_disable_service: bool = False
223
+ # Do not use setproctitle for internal process in legacy service.
224
+ x_disable_setproctitle: bool = False
225
+ # Disable system metrics collection.
226
+ x_disable_stats: bool = False
227
+ # Disable check for latest version of wandb, from PyPI.
228
+ x_disable_update_check: bool = False
229
+ # Prevent early viewer query.
230
+ x_disable_viewer: bool = False
231
+ # Disable automatic machine info collection.
232
+ x_disable_machine_info: bool = False
233
+ # Python executable
234
+ x_executable: str | None = None
235
+ # Additional headers to add to all outgoing HTTP requests.
236
+ x_extra_http_headers: dict[str, str] | None = None
237
+ # An approximate maximum request size for the filestream API.
238
+ #
239
+ # This applies when wandb-core is enabled. Its purpose is to prevent
240
+ # HTTP requests from failing due to containing too much data.
241
+ #
242
+ # This number is approximate: requests will be slightly larger.
243
+ x_file_stream_max_bytes: int | None = None
244
+ # Max line length for filestream jsonl files.
245
+ x_file_stream_max_line_bytes: int | None = None
246
+ # Interval in seconds between filestream transmissions.
247
+ x_file_stream_transmit_interval: float | None = None
248
+ # Filestream retry client configuration.
249
+ # max number of retries
250
+ x_file_stream_retry_max: int | None = None
251
+ # min wait time between retries
252
+ x_file_stream_retry_wait_min_seconds: float | None = None
253
+ # max wait time between retries
254
+ x_file_stream_retry_wait_max_seconds: float | None = None
255
+ # timeout for individual HTTP requests
256
+ x_file_stream_timeout_seconds: float | None = None
257
+ # file transfer retry client configuration
258
+ x_file_transfer_retry_max: int | None = None
259
+ x_file_transfer_retry_wait_min_seconds: float | None = None
260
+ x_file_transfer_retry_wait_max_seconds: float | None = None
261
+ x_file_transfer_timeout_seconds: float | None = None
262
+ # override setting for the computed files_dir
263
+ x_files_dir: str | None = None
264
+ # flow control configuration for file stream
265
+ x_flow_control_custom: bool | None = None
266
+ x_flow_control_disabled: bool | None = None
267
+ # graphql retry client configuration
268
+ x_graphql_retry_max: int | None = None
269
+ x_graphql_retry_wait_min_seconds: float | None = None
270
+ x_graphql_retry_wait_max_seconds: float | None = None
271
+ x_graphql_timeout_seconds: float | None = None
272
+ x_internal_check_process: float = 8.0
273
+ x_jupyter_name: str | None = None
274
+ x_jupyter_path: str | None = None
275
+ x_jupyter_root: str | None = None
276
+ # Label to assign to system metrics and console logs collected for the run
277
+ # to group by on the frontend. Can be used to distinguish data from different
278
+ # nodes in a distributed training job.
279
+ x_label: str | None = None
280
+ x_live_policy_rate_limit: int | None = None
281
+ x_live_policy_wait_time: int | None = None
282
+ x_log_level: int = logging.INFO
283
+ x_network_buffer: int | None = None
284
+ # Determines whether to save internal wandb files and metadata.
285
+ # In a distributed setting, this is useful for avoiding file overwrites on secondary nodes
286
+ # when only system metrics and logs are needed, as the primary node handles the main logging.
287
+ x_primary_node: bool = True
288
+ # [deprecated, use http(s)_proxy] custom proxy servers for the requests to W&B
289
+ # [scheme -> url].
290
+ x_proxies: dict[str, str] | None = None
291
+ x_runqueue_item_id: str | None = None
292
+ x_require_legacy_service: bool = False
293
+ x_save_requirements: bool = True
294
+ x_service_transport: str | None = None
295
+ x_service_wait: float = 30.0
296
+ x_show_operation_stats: bool = False
297
+ # The start time of the run in seconds since the Unix epoch.
298
+ x_start_time: float | None = None
299
+ # PID of the process that started the wandb-core process to collect system stats for.
300
+ x_stats_pid: int = os.getpid()
301
+ # Sampling interval for the system monitor in seconds.
302
+ x_stats_sampling_interval: float = Field(default=10.0)
303
+ # Path to store the default config file for the neuron-monitor tool
304
+ # used to monitor AWS Trainium devices.
305
+ x_stats_neuron_monitor_config_path: str | None = None
306
+ # Open metrics endpoint names and urls.
307
+ x_stats_open_metrics_endpoints: dict[str, str] | None = None
308
+ # Filter to apply to metrics collected from OpenMetrics endpoints.
309
+ # Supports two formats:
379
310
  # - {"metric regex pattern, including endpoint name as prefix": {"label": "label value regex pattern"}}
380
311
  # - ("metric regex pattern 1", "metric regex pattern 2", ...)
381
- _stats_open_metrics_filters: Union[Sequence[str], Mapping[str, Mapping[str, str]]]
382
- _stats_disk_paths: Sequence[str] # paths to monitor disk usage
383
- _stats_buffer_size: int # number of consolidated samples to buffer before flushing, available in run obj
384
- _tmp_code_dir: str
385
- _tracelog: str
386
- _unsaved_keys: Sequence[str]
387
- _windows: bool
388
- _show_operation_stats: bool
389
- allow_val_change: bool
390
- anonymous: str
391
- api_key: str
392
- azure_account_url_to_access_key: Dict[str, str]
393
- base_url: str # The base url for the wandb api
394
- code_dir: str
395
- colab_url: str
396
- config_paths: Sequence[str]
397
- console: str
398
- console_multipart: bool # whether to produce multipart console log files
399
- credentials_file: str # file path to write access tokens
400
- deployment: str
401
- disable_code: bool
402
- disable_git: bool
403
- disable_hints: bool
404
- disable_job_creation: bool
405
- disabled: bool # Alias for mode=dryrun, not supported yet
406
- docker: str
407
- email: str
408
- entity: str
409
- files_dir: str
410
- force: bool
411
- fork_from: RunMoment
412
- resume_from: RunMoment
413
- git_commit: str
414
- git_remote: str
415
- git_remote_url: str
416
- git_root: str
417
- heartbeat_seconds: int
418
- host: str
419
- http_proxy: str # proxy server for the http requests to W&B
420
- https_proxy: str # proxy server for the https requests to W&B
421
- identity_token_file: str # file path to supply a jwt for authentication
422
- ignore_globs: Tuple[str]
423
- init_timeout: float
424
- is_local: bool
425
- job_name: str
426
- job_source: str
427
- label_disable: bool
428
- launch: bool
429
- launch_config_path: str
430
- log_dir: str
431
- log_internal: str
432
- log_symlink_internal: str
433
- log_symlink_user: str
434
- log_user: str
435
- login_timeout: float
436
- # magic: Union[str, bool, dict] # never used in code, deprecated
437
- mode: str
438
- notebook_name: str
439
- program: str
440
- program_abspath: str
441
- program_relpath: str
442
- project: str
443
- project_url: str
444
- quiet: bool
445
- reinit: bool
446
- relogin: bool
447
- # todo: add a preprocessing step to convert this to string
448
- resume: Union[str, bool]
449
- resume_fname: str
450
- resumed: bool # indication from the server about the state of the run (different from resume - user provided flag)
451
- root_dir: str
452
- run_group: str
453
- run_id: str
454
- run_job_type: str
455
- run_mode: str
456
- run_name: str
457
- run_notes: str
458
- run_tags: Tuple[str]
459
- run_url: str
460
- sagemaker_disable: bool
461
- save_code: bool
462
- settings_system: str
463
- settings_workspace: str
464
- show_colors: bool
465
- show_emoji: bool
466
- show_errors: bool
467
- show_info: bool
468
- show_warnings: bool
469
- silent: bool
470
- start_method: str
471
- strict: bool
472
- summary_errors: int
473
- summary_timeout: int
474
- summary_warnings: int
475
- sweep_id: str
476
- sweep_param_path: str
477
- sweep_url: str
478
- symlink: bool
479
- sync_dir: str
480
- sync_file: str
481
- sync_symlink_latest: str
482
- table_raise_on_max_row_limit_exceeded: bool
483
- timespec: str
484
- tmp_dir: str
485
- username: str
486
- wandb_dir: str
487
-
488
-
489
- class Property:
490
- """A class to represent attributes (individual settings) of the Settings object.
491
-
492
- - Encapsulates the logic of how to preprocess and validate values of settings
493
- throughout the lifetime of a class instance.
494
- - Allows for runtime modification of settings with hooks, e.g. in the case when
495
- a setting depends on another setting.
496
- - The update() method is used to update the value of a setting.
497
- - The `is_policy` attribute determines the source priority when updating the property value.
498
- E.g. if `is_policy` is True, the smallest `Source` value takes precedence.
499
- """
500
-
501
- def __init__( # pylint: disable=unused-argument
502
- self,
503
- name: str,
504
- value: Optional[Any] = None,
505
- preprocessor: Union[Callable, Sequence[Callable], None] = None,
506
- # validators allow programming by contract
507
- validator: Union[Callable, Sequence[Callable], None] = None,
508
- # runtime converter (hook): properties can be e.g. tied to other properties
509
- hook: Union[Callable, Sequence[Callable], None] = None,
510
- # always apply hook even if value is None. can be used to replace @property's
511
- auto_hook: bool = False,
512
- is_policy: bool = False,
513
- frozen: bool = False,
514
- source: int = Source.BASE,
515
- **kwargs: Any,
516
- ):
517
- self.name = name
518
- self._preprocessor = preprocessor
519
- self._validator = validator
520
- self._hook = hook
521
- self._auto_hook = auto_hook
522
- self._is_policy = is_policy
523
- self._source = source
524
-
525
- # preprocess and validate value
526
- self._value = self._validate(self._preprocess(value))
527
-
528
- self.__frozen = frozen
312
+ x_stats_open_metrics_filters: dict[str, dict[str, str]] | Sequence[str] | None = (
313
+ None
314
+ )
315
+ # HTTP headers to add to OpenMetrics requests.
316
+ x_stats_open_metrics_http_headers: dict[str, str] | None = None
317
+ # System paths to monitor for disk usage.
318
+ x_stats_disk_paths: Sequence[str] | None = Field(
319
+ default_factory=lambda: ("/", "/System/Volumes/Data")
320
+ if platform.system() == "Darwin"
321
+ else ("/",)
322
+ )
323
+ # Number of system metric samples to buffer in memory in the wandb-core process.
324
+ # Can be accessed via run._system_metrics.
325
+ x_stats_buffer_size: int = 0
326
+ # Flag to indicate whether we are syncing a run from the transaction log.
327
+ x_sync: bool = False
328
+ # Controls whether this process can update the run's final state (finished/failed) on the server.
329
+ # Set to False in distributed training when only the main process should determine the final state.
330
+ x_update_finish_state: bool = True
331
+
332
+ # Model validator to catch legacy settings.
333
+ @model_validator(mode="before")
334
+ @classmethod
335
+ def catch_private_settings(cls, values):
336
+ """Check if a private field is provided and assign to the corrsponding public one.
337
+
338
+ This is a compatibility layer to handle previous versions of the settings.
339
+ """
340
+ new_values = {}
341
+ for key in values:
342
+ # Internal settings are prefixed with "x_" instead of "_"
343
+ # as Pydantic does not allow "_" in field names.
344
+ if key.startswith("_"):
345
+ new_values["x" + key] = values[key]
346
+ else:
347
+ new_values[key] = values[key]
348
+ return new_values
529
349
 
530
- @property
531
- def value(self) -> Any:
532
- """Apply the runtime modifier(s) (if any) and return the value."""
533
- _value = self._value
534
- if (_value is not None or self._auto_hook) and self._hook is not None:
535
- _hook = [self._hook] if callable(self._hook) else self._hook
536
- for h in _hook:
537
- _value = h(_value)
538
- return _value
350
+ @model_validator(mode="after")
351
+ def validate_mutual_exclusion_of_branching_args(self) -> Self:
352
+ if (
353
+ sum(
354
+ o is not None
355
+ for o in [
356
+ self.fork_from,
357
+ self.resume,
358
+ self.resume_from,
359
+ ]
360
+ )
361
+ > 1
362
+ ):
363
+ raise ValueError(
364
+ "`fork_from`, `resume`, or `resume_from` are mutually exclusive. "
365
+ "Please specify only one of them."
366
+ )
367
+ return self
539
368
 
540
- @property
541
- def is_policy(self) -> bool:
542
- return self._is_policy
369
+ # Field validators.
543
370
 
544
- @property
545
- def source(self) -> int:
546
- return self._source
547
-
548
- def _preprocess(self, value: Any) -> Any:
549
- if value is not None and self._preprocessor is not None:
550
- _preprocessor = (
551
- [self._preprocessor]
552
- if callable(self._preprocessor)
553
- else self._preprocessor
371
+ @field_validator("x_disable_service", mode="after")
372
+ @classmethod
373
+ def validate_disable_service(cls, value):
374
+ if value:
375
+ termwarn(
376
+ "Disabling the wandb service is deprecated as of version 0.18.0 "
377
+ "and will be removed in future versions. ",
378
+ repeat=False,
554
379
  )
555
- for p in _preprocessor:
556
- try:
557
- value = p(value)
558
- except Exception:
559
- raise SettingsPreprocessingError(
560
- f"Unable to preprocess value for property {self.name}: {value}."
561
- )
562
380
  return value
563
381
 
564
- def _validate(self, value: Any) -> Any:
565
- if value is not None and self._validator is not None:
566
- _validator = (
567
- [self._validator] if callable(self._validator) else self._validator
568
- )
569
- for v in _validator:
570
- if not v(value):
571
- # failed validation will likely cause a downstream error
572
- # when trying to convert to protobuf, so we raise a hard error
573
- raise SettingsValidationError(
574
- f"Invalid value for property {self.name}: {value}."
575
- )
382
+ @field_validator("api_key", mode="after")
383
+ @classmethod
384
+ def validate_api_key(cls, value):
385
+ if value is not None and (len(value) > len(value.strip())):
386
+ raise UsageError("API key cannot start or end with whitespace")
576
387
  return value
577
388
 
578
- def update(self, value: Any, source: int = Source.OVERRIDE) -> None:
579
- """Update the value of the property."""
580
- if self.__frozen:
581
- raise TypeError("Property object is frozen")
582
- # - always update value if source == Source.OVERRIDE
583
- # - if not previously overridden:
584
- # - update value if source is lower than or equal to current source and property is policy
585
- # - update value if source is higher than or equal to current source and property is not policy
586
- if (
587
- (source == Source.OVERRIDE)
588
- or (
589
- self._is_policy
590
- and self._source != Source.OVERRIDE
591
- and source <= self._source
592
- )
593
- or (
594
- not self._is_policy
595
- and self._source != Source.OVERRIDE
596
- and source >= self._source
389
+ @field_validator("base_url", mode="after")
390
+ @classmethod
391
+ def validate_base_url(cls, value):
392
+ cls.validate_url(value)
393
+ # wandb.ai-specific checks
394
+ if re.match(r".*wandb\.ai[^\.]*$", value) and "api." not in value:
395
+ # user might guess app.wandb.ai or wandb.ai is the default cloud server
396
+ raise ValueError(
397
+ f"{value} is not a valid server address, did you mean https://api.wandb.ai?"
597
398
  )
399
+ elif re.match(r".*wandb\.ai[^\.]*$", value) and not value.startswith("https"):
400
+ raise ValueError("http is not secure, please use https://api.wandb.ai")
401
+ return value.rstrip("/")
402
+
403
+ @field_validator("console", mode="after")
404
+ @classmethod
405
+ def validate_console(cls, value, info):
406
+ if value != "auto":
407
+ return value
408
+ if (
409
+ ipython.in_jupyter()
410
+ or (info.data.get("start_method") == "thread")
411
+ or not info.data.get("disable_service")
412
+ or platform.system() == "Windows"
598
413
  ):
599
- # self.__dict__["_value"] = self._validate(self._preprocess(value))
600
- self._value = self._validate(self._preprocess(value))
601
- self._source = source
602
-
603
- def __setattr__(self, key: str, value: Any) -> None:
604
- if "_Property__frozen" in self.__dict__ and self.__frozen:
605
- raise TypeError(f"Property object {self.name} is frozen")
606
- if key == "value":
607
- raise AttributeError("Use update() to update property value")
608
- self.__dict__[key] = value
609
-
610
- def __str__(self) -> str:
611
- return f"{self.value!r}" if isinstance(self.value, str) else f"{self.value}"
612
-
613
- def __repr__(self) -> str:
614
- return (
615
- f"<Property {self.name}: value={self.value} "
616
- f"_value={self._value} source={self._source} is_policy={self._is_policy}>"
617
- )
618
- # return f"<Property {self.name}: value={self.value}>"
619
- # return self.__dict__.__repr__()
414
+ value = "wrap"
415
+ else:
416
+ value = "redirect"
417
+ return value
620
418
 
419
+ @field_validator("x_file_stream_max_line_bytes", mode="after")
420
+ @classmethod
421
+ def validate_file_stream_max_line_bytes(cls, value):
422
+ if value is not None and value < 1:
423
+ raise ValueError("File stream max line bytes must be greater than 0")
424
+ return value
621
425
 
622
- class Settings(SettingsData):
623
- """Settings for the W&B SDK."""
426
+ @field_validator("fork_from", mode="before")
427
+ @classmethod
428
+ def validate_fork_from(cls, value, info) -> RunMoment | None:
429
+ run_moment = cls._runmoment_preprocessor(value)
430
+ if run_moment and info.data.get("run_id") == run_moment.run:
431
+ raise ValueError(
432
+ "Provided `run_id` is the same as the run to `fork_from`. "
433
+ "Please provide a different `run_id` or remove the `run_id` argument. "
434
+ "If you want to rewind the current run, please use `resume_from` instead."
435
+ )
436
+ return run_moment
624
437
 
625
- def _default_props(self) -> Dict[str, Dict[str, Any]]:
626
- """Initialize instance attributes (individual settings) as Property objects.
438
+ @field_validator("http_proxy", mode="after")
439
+ @classmethod
440
+ def validate_http_proxy(cls, value):
441
+ if value is None:
442
+ return None
443
+ cls.validate_url(value)
444
+ return value.rstrip("/")
627
445
 
628
- Helper method that is used in `__init__` together with the class attributes.
629
- Note that key names must be the same as the class attribute names.
630
- """
631
- props: Dict[str, Dict[str, Any]] = dict(
632
- _aws_lambda={
633
- "hook": lambda _: is_aws_lambda(),
634
- "auto_hook": True,
635
- },
636
- _code_path_local={
637
- "hook": lambda _: _get_program_relpath(self.program),
638
- "auto_hook": True,
639
- },
640
- _colab={
641
- "hook": lambda _: "google.colab" in sys.modules,
642
- "auto_hook": True,
643
- },
644
- _disable_machine_info={
645
- "value": False,
646
- "preprocessor": _str_as_bool,
647
- },
648
- _disable_meta={
649
- "value": False,
650
- "preprocessor": _str_as_bool,
651
- "hook": lambda x: self._disable_machine_info or x,
652
- },
653
- _disable_service={
654
- "value": False,
655
- "preprocessor": self._process_disable_service,
656
- "is_policy": True,
657
- },
658
- _disable_setproctitle={"value": False, "preprocessor": _str_as_bool},
659
- _disable_stats={
660
- "value": False,
661
- "preprocessor": _str_as_bool,
662
- "hook": lambda x: self._disable_machine_info or x,
663
- },
664
- _disable_update_check={"preprocessor": _str_as_bool},
665
- _disable_viewer={"preprocessor": _str_as_bool},
666
- _extra_http_headers={"preprocessor": _str_as_json},
667
- _file_stream_max_bytes={"preprocessor": int},
668
- _file_stream_transmit_interval={"preprocessor": float},
669
- _file_stream_retry_max={"preprocessor": int},
670
- _file_stream_retry_wait_min_seconds={"preprocessor": float},
671
- _file_stream_retry_wait_max_seconds={"preprocessor": float},
672
- _file_stream_timeout_seconds={"preprocessor": float},
673
- _file_transfer_retry_max={"preprocessor": int},
674
- _file_transfer_retry_wait_min_seconds={"preprocessor": float},
675
- _file_transfer_retry_wait_max_seconds={"preprocessor": float},
676
- _file_transfer_timeout_seconds={"preprocessor": float},
677
- _flow_control_disabled={
678
- "hook": lambda _: self._network_buffer == 0,
679
- "auto_hook": True,
680
- },
681
- _flow_control_custom={
682
- "hook": lambda _: bool(self._network_buffer),
683
- "auto_hook": True,
684
- },
685
- _graphql_retry_max={"preprocessor": int},
686
- _graphql_retry_wait_min_seconds={"preprocessor": float},
687
- _graphql_retry_wait_max_seconds={"preprocessor": float},
688
- _graphql_timeout_seconds={"preprocessor": float},
689
- _internal_check_process={"value": 8, "preprocessor": float},
690
- _internal_queue_timeout={"value": 2, "preprocessor": float},
691
- _ipython={
692
- "hook": lambda _: ipython.in_ipython(),
693
- "auto_hook": True,
694
- },
695
- _jupyter={
696
- "hook": lambda _: ipython.in_jupyter(),
697
- "auto_hook": True,
698
- },
699
- _kaggle={"hook": lambda _: util._is_likely_kaggle(), "auto_hook": True},
700
- _log_level={"value": logging.DEBUG},
701
- _network_buffer={"preprocessor": int},
702
- _noop={"hook": lambda _: self.mode == "disabled", "auto_hook": True},
703
- _notebook={
704
- "hook": lambda _: self._ipython
705
- or self._jupyter
706
- or self._colab
707
- or self._kaggle,
708
- "auto_hook": True,
709
- },
710
- _offline={
711
- "hook": (
712
- lambda _: True
713
- if self.disabled or (self.mode in ("dryrun", "offline"))
714
- else False
715
- ),
716
- "auto_hook": True,
717
- },
718
- _platform={"value": util.get_platform_name()},
719
- _proxies={
720
- # TODO: deprecate and ask the user to use http_proxy and https_proxy instead
721
- "preprocessor": _str_as_json,
722
- },
723
- _require_legacy_service={"value": False, "preprocessor": _str_as_bool},
724
- _save_requirements={"value": True, "preprocessor": _str_as_bool},
725
- _service_wait={
726
- "value": 30,
727
- "preprocessor": float,
728
- "validator": self._validate__service_wait,
729
- },
730
- _shared={
731
- "hook": lambda _: self.mode == "shared",
732
- "auto_hook": True,
733
- },
734
- _start_datetime={"preprocessor": _datetime_as_str},
735
- _stats_sampling_interval={
736
- "value": 10.0,
737
- "preprocessor": float,
738
- "validator": self._validate__stats_sampling_interval,
739
- },
740
- _stats_sample_rate_seconds={
741
- "value": 2.0,
742
- "preprocessor": float,
743
- "validator": self._validate__stats_sample_rate_seconds,
744
- },
745
- _stats_samples_to_average={
746
- "value": 15,
747
- "preprocessor": int,
748
- "validator": self._validate__stats_samples_to_average,
749
- },
750
- _stats_join_assets={"value": True, "preprocessor": _str_as_bool},
751
- _stats_neuron_monitor_config_path={
752
- "hook": lambda x: self._path_convert(x),
753
- },
754
- _stats_open_metrics_endpoints={
755
- "preprocessor": _str_as_json,
756
- },
757
- _stats_open_metrics_filters={
758
- # capture all metrics on all endpoints by default
759
- "value": (".*",),
760
- "preprocessor": _str_as_json,
761
- },
762
- _stats_disk_paths={
763
- "value": ("/",),
764
- "preprocessor": _str_as_json,
765
- },
766
- _stats_buffer_size={
767
- "value": 0,
768
- "preprocessor": int,
769
- },
770
- _sync={"value": False},
771
- _tmp_code_dir={
772
- "value": "code",
773
- "hook": lambda x: self._path_convert(self.tmp_dir, x),
774
- },
775
- _windows={
776
- "hook": lambda _: platform.system() == "Windows",
777
- "auto_hook": True,
778
- },
779
- _show_operation_stats={"preprocessor": _str_as_bool},
780
- anonymous={"validator": self._validate_anonymous},
781
- api_key={"validator": self._validate_api_key},
782
- base_url={
783
- "value": "https://api.wandb.ai",
784
- "preprocessor": lambda x: str(x).strip().rstrip("/"),
785
- "validator": self._validate_base_url,
786
- },
787
- colab_url={
788
- "hook": lambda _: self._get_colab_url(),
789
- "auto_hook": True,
790
- },
791
- config_paths={"preprocessor": _str_as_tuple},
792
- console={
793
- "value": "auto",
794
- "validator": self._validate_console,
795
- "hook": lambda x: self._convert_console(x),
796
- "auto_hook": True,
797
- },
798
- console_multipart={"value": False, "preprocessor": _str_as_bool},
799
- credentials_file={
800
- "value": str(credentials.DEFAULT_WANDB_CREDENTIALS_FILE),
801
- "preprocessor": str,
802
- },
803
- deployment={
804
- "hook": lambda _: "local" if self.is_local else "cloud",
805
- "auto_hook": True,
806
- },
807
- disable_code={
808
- "value": False,
809
- "preprocessor": _str_as_bool,
810
- "hook": lambda x: self._disable_machine_info or x,
811
- },
812
- disable_hints={"preprocessor": _str_as_bool},
813
- disable_git={
814
- "value": False,
815
- "preprocessor": _str_as_bool,
816
- "hook": lambda x: self._disable_machine_info or x,
817
- },
818
- disable_job_creation={
819
- "value": False,
820
- "preprocessor": _str_as_bool,
821
- "hook": lambda x: self._disable_machine_info or x,
822
- },
823
- disabled={"value": False, "preprocessor": _str_as_bool},
824
- files_dir={
825
- "value": "files",
826
- "hook": lambda x: self._path_convert(
827
- self.wandb_dir, f"{self.run_mode}-{self.timespec}-{self.run_id}", x
828
- ),
829
- },
830
- force={"preprocessor": _str_as_bool},
831
- fork_from={
832
- "value": None,
833
- "preprocessor": _runmoment_preprocessor,
834
- },
835
- resume_from={
836
- "value": None,
837
- "preprocessor": _runmoment_preprocessor,
838
- },
839
- git_remote={"value": "origin"},
840
- heartbeat_seconds={"value": 30},
841
- http_proxy={
842
- "hook": lambda x: self._proxies and self._proxies.get("http") or x,
843
- "auto_hook": True,
844
- },
845
- https_proxy={
846
- "hook": lambda x: self._proxies and self._proxies.get("https") or x,
847
- "auto_hook": True,
848
- },
849
- identity_token_file={"value": None, "preprocessor": str},
850
- ignore_globs={
851
- "value": tuple(),
852
- "preprocessor": lambda x: tuple(x) if not isinstance(x, tuple) else x,
853
- },
854
- init_timeout={"value": 90, "preprocessor": lambda x: float(x)},
855
- is_local={
856
- "hook": (
857
- lambda _: self.base_url != "https://api.wandb.ai"
858
- if self.base_url is not None
859
- else False
860
- ),
861
- "auto_hook": True,
862
- },
863
- job_name={"preprocessor": str},
864
- job_source={"validator": self._validate_job_source},
865
- label_disable={"preprocessor": _str_as_bool},
866
- launch={"preprocessor": _str_as_bool},
867
- log_dir={
868
- "value": "logs",
869
- "hook": lambda x: self._path_convert(
870
- self.wandb_dir, f"{self.run_mode}-{self.timespec}-{self.run_id}", x
871
- ),
872
- },
873
- log_internal={
874
- "value": "debug-internal.log",
875
- "hook": lambda x: self._path_convert(self.log_dir, x),
876
- },
877
- log_symlink_internal={
878
- "value": "debug-internal.log",
879
- "hook": lambda x: self._path_convert(self.wandb_dir, x),
880
- },
881
- log_symlink_user={
882
- "value": "debug.log",
883
- "hook": lambda x: self._path_convert(self.wandb_dir, x),
884
- },
885
- log_user={
886
- "value": "debug.log",
887
- "hook": lambda x: self._path_convert(self.log_dir, x),
888
- },
889
- login_timeout={"preprocessor": lambda x: float(x)},
890
- mode={"value": "online", "validator": self._validate_mode},
891
- program={
892
- "hook": lambda x: self._get_program(x),
893
- },
894
- project={
895
- "validator": self._validate_project,
896
- },
897
- project_url={"hook": lambda _: self._project_url(), "auto_hook": True},
898
- quiet={"preprocessor": _str_as_bool},
899
- reinit={"preprocessor": _str_as_bool},
900
- relogin={"preprocessor": _str_as_bool},
901
- # todo: hack to make to_proto() always happy
902
- resume={"preprocessor": lambda x: None if x is False else x},
903
- resume_fname={
904
- "value": "wandb-resume.json",
905
- "hook": lambda x: self._path_convert(self.wandb_dir, x),
906
- },
907
- resumed={"value": "False", "preprocessor": _str_as_bool},
908
- root_dir={
909
- "preprocessor": lambda x: str(x),
910
- "value": os.path.abspath(os.getcwd()),
911
- },
912
- run_id={
913
- "validator": self._validate_run_id,
914
- },
915
- run_mode={
916
- "hook": lambda _: "offline-run" if self._offline else "run",
917
- "auto_hook": True,
918
- },
919
- run_tags={
920
- "preprocessor": lambda x: tuple(x) if not isinstance(x, tuple) else x,
921
- },
922
- run_url={"hook": lambda _: self._run_url(), "auto_hook": True},
923
- sagemaker_disable={"preprocessor": _str_as_bool},
924
- save_code={"preprocessor": _str_as_bool},
925
- settings_system={
926
- "value": os.path.join("~", ".config", "wandb", "settings"),
927
- "hook": lambda x: self._path_convert(x),
928
- },
929
- settings_workspace={
930
- "value": "settings",
931
- "hook": lambda x: self._path_convert(self.wandb_dir, x),
932
- },
933
- show_colors={"preprocessor": _str_as_bool},
934
- show_emoji={"preprocessor": _str_as_bool},
935
- show_errors={"value": "True", "preprocessor": _str_as_bool},
936
- show_info={"value": "True", "preprocessor": _str_as_bool},
937
- show_warnings={"value": "True", "preprocessor": _str_as_bool},
938
- silent={"value": "False", "preprocessor": _str_as_bool},
939
- start_method={"validator": self._validate_start_method},
940
- strict={"preprocessor": _str_as_bool},
941
- summary_timeout={"value": 60, "preprocessor": lambda x: int(x)},
942
- summary_warnings={
943
- "value": 5,
944
- "preprocessor": lambda x: int(x),
945
- "is_policy": True,
946
- },
947
- sweep_url={"hook": lambda _: self._sweep_url(), "auto_hook": True},
948
- symlink={"preprocessor": _str_as_bool},
949
- sync_dir={
950
- "hook": [
951
- lambda _: self._path_convert(
952
- self.wandb_dir, f"{self.run_mode}-{self.timespec}-{self.run_id}"
953
- )
954
- ],
955
- "auto_hook": True,
956
- },
957
- sync_file={
958
- "hook": lambda _: self._path_convert(
959
- self.sync_dir, f"run-{self.run_id}.wandb"
960
- ),
961
- "auto_hook": True,
962
- },
963
- sync_symlink_latest={
964
- "value": "latest-run",
965
- "hook": lambda x: self._path_convert(self.wandb_dir, x),
966
- },
967
- table_raise_on_max_row_limit_exceeded={
968
- "value": False,
969
- "preprocessor": _str_as_bool,
970
- },
971
- timespec={
972
- "hook": lambda _: self._start_datetime,
973
- "auto_hook": True,
974
- },
975
- tmp_dir={
976
- "value": "tmp",
977
- "hook": lambda x: (
978
- self._path_convert(
979
- self.wandb_dir,
980
- f"{self.run_mode}-{self.timespec}-{self.run_id}",
981
- x,
982
- )
983
- or tempfile.gettempdir()
984
- ),
985
- },
986
- wandb_dir={
987
- "hook": lambda _: _get_wandb_dir(self.root_dir or ""),
988
- "auto_hook": True,
989
- },
990
- )
991
- return props
446
+ @field_validator("https_proxy", mode="after")
447
+ @classmethod
448
+ def validate_https_proxy(cls, value):
449
+ if value is None:
450
+ return None
451
+ cls.validate_url(value)
452
+ return value.rstrip("/")
992
453
 
993
- # helper methods for validating values
994
- @staticmethod
995
- def _validator_factory(hint: Any) -> Callable[[Any], bool]: # noqa: C901
996
- """Return a factory for setting type validators."""
454
+ @field_validator("ignore_globs", mode="after")
455
+ @classmethod
456
+ def validate_ignore_globs(cls, value):
457
+ return tuple(value) if not isinstance(value, tuple) else value
997
458
 
998
- def helper(value: Any) -> bool:
999
- try:
1000
- is_valid = is_instance_recursive(value, hint)
1001
- except Exception:
1002
- # instance check failed, but let's not crash and only print a warning
1003
- is_valid = False
459
+ @field_validator("project", mode="after")
460
+ @classmethod
461
+ def validate_project(cls, value, info):
462
+ if value is None:
463
+ return None
464
+ invalid_chars_list = list("/\\#?%:")
465
+ if len(value) > 128:
466
+ raise UsageError(f"Invalid project name {value!r}: exceeded 128 characters")
467
+ invalid_chars = {char for char in invalid_chars_list if char in value}
468
+ if invalid_chars:
469
+ raise UsageError(
470
+ f"Invalid project name {value!r}: "
471
+ f"cannot contain characters {','.join(invalid_chars_list)!r}, "
472
+ f"found {','.join(invalid_chars)!r}"
473
+ )
474
+ return value
1004
475
 
1005
- return is_valid
476
+ @field_validator("resume", mode="before")
477
+ @classmethod
478
+ def validate_resume(cls, value):
479
+ if value is False:
480
+ return None
481
+ if value is True:
482
+ return "auto"
483
+ return value
1006
484
 
1007
- return helper
485
+ @field_validator("resume_from", mode="before")
486
+ @classmethod
487
+ def validate_resume_from(cls, value, info) -> RunMoment | None:
488
+ run_moment = cls._runmoment_preprocessor(value)
489
+ if run_moment and info.data.get("run_id") != run_moment.run:
490
+ raise ValueError(
491
+ "Both `run_id` and `resume_from` have been specified with different ids."
492
+ )
493
+ return run_moment
1008
494
 
1009
- @staticmethod
1010
- def _validate_mode(value: str) -> bool:
1011
- choices: Set[str] = {"dryrun", "run", "offline", "online", "disabled", "shared"}
1012
- if value not in choices:
1013
- raise UsageError(f"Settings field `mode`: {value!r} not in {choices}")
1014
- return True
495
+ @field_validator("run_id", mode="after")
496
+ @classmethod
497
+ def validate_run_id(cls, value, info):
498
+ if value is None:
499
+ return None
1015
500
 
1016
- @staticmethod
1017
- def _validate_project(value: Optional[str]) -> bool:
1018
- invalid_chars_list = list("/\\#?%:")
1019
- if value is not None:
1020
- if len(value) > 128:
1021
- raise UsageError(
1022
- f"Invalid project name {value!r}: exceeded 128 characters"
1023
- )
1024
- invalid_chars = {char for char in invalid_chars_list if char in value}
1025
- if invalid_chars:
1026
- raise UsageError(
1027
- f"Invalid project name {value!r}: "
1028
- f"cannot contain characters {','.join(invalid_chars_list)!r}, "
1029
- f"found {','.join(invalid_chars)!r}"
1030
- )
1031
- return True
501
+ if len(value) == 0:
502
+ raise UsageError("Run ID cannot be empty")
503
+ if len(value) > len(value.strip()):
504
+ raise UsageError("Run ID cannot start or end with whitespace")
505
+ if not bool(value.strip()):
506
+ raise UsageError("Run ID cannot contain only whitespace")
507
+ return value
1032
508
 
1033
- @staticmethod
1034
- def _validate_start_method(value: str) -> bool:
509
+ @field_validator("settings_system", mode="after")
510
+ @classmethod
511
+ def validate_settings_system(cls, value):
512
+ return _path_convert(value)
513
+
514
+ @field_validator("x_service_wait", mode="after")
515
+ @classmethod
516
+ def validate_service_wait(cls, value):
517
+ if value < 0:
518
+ raise UsageError("Service wait time cannot be negative")
519
+ return
520
+
521
+ @field_validator("start_method")
522
+ @classmethod
523
+ def validate_start_method(cls, value):
524
+ if value is None:
525
+ return value
1035
526
  available_methods = ["thread"]
1036
527
  if hasattr(multiprocessing, "get_all_start_methods"):
1037
528
  available_methods += multiprocessing.get_all_start_methods()
@@ -1039,251 +530,237 @@ class Settings(SettingsData):
1039
530
  raise UsageError(
1040
531
  f"Settings field `start_method`: {value!r} not in {available_methods}"
1041
532
  )
1042
- return True
1043
-
1044
- @staticmethod
1045
- def _validate_console(value: str) -> bool:
1046
- choices = ConsoleValue
1047
- if value not in choices:
1048
- # do not advertise internal console states
1049
- choices -= {"wrap_emu", "wrap_raw"}
1050
- raise UsageError(f"Settings field `console`: {value!r} not in {choices}")
1051
- return True
533
+ return value
1052
534
 
1053
- @staticmethod
1054
- def _validate_anonymous(value: str) -> bool:
1055
- choices: Set[str] = {"allow", "must", "never", "false", "true"}
1056
- if value not in choices:
1057
- raise UsageError(f"Settings field `anonymous`: {value!r} not in {choices}")
1058
- return True
535
+ @field_validator("x_stats_sampling_interval", mode="after")
536
+ @classmethod
537
+ def validate_stats_sampling_interval(cls, value):
538
+ if value < 0.1:
539
+ raise UsageError("Stats sampling interval cannot be less than 0.1 seconds")
540
+ return value
1059
541
 
1060
- @staticmethod
1061
- def _validate_run_id(value: str) -> bool:
1062
- # if len(value) > len(value.strip()):
1063
- # raise UsageError("Run ID cannot start or end with whitespace")
1064
- return bool(value.strip())
542
+ @field_validator("x_stats_open_metrics_endpoints", mode="before")
543
+ @classmethod
544
+ def validate_stats_open_metrics_endpoints(cls, value):
545
+ if isinstance(value, str):
546
+ return json.loads(value)
547
+ return value
1065
548
 
1066
- @staticmethod
1067
- def _validate_api_key(value: str) -> bool:
1068
- if len(value) > len(value.strip()):
1069
- raise UsageError("API key cannot start or end with whitespace")
549
+ @field_validator("x_stats_open_metrics_filters", mode="before")
550
+ @classmethod
551
+ def validate_stats_open_metrics_filters(cls, value):
552
+ if isinstance(value, str):
553
+ return json.loads(value)
554
+ return value
1070
555
 
1071
- # todo: move this check to the post-init validation step
1072
- # if value.startswith("local") and not self.is_local:
1073
- # raise UsageError(
1074
- # "Attempting to use a local API key to connect to https://api.wandb.ai"
1075
- # )
1076
- # todo: move here the logic from sdk/lib/apikey.py
556
+ @field_validator("x_stats_open_metrics_http_headers", mode="before")
557
+ @classmethod
558
+ def validate_stats_open_metrics_http_headers(cls, value):
559
+ if isinstance(value, str):
560
+ return json.loads(value)
561
+ return value
1077
562
 
1078
- return True
563
+ @field_validator("sweep_id", mode="after")
564
+ @classmethod
565
+ def validate_sweep_id(cls, value):
566
+ if value is None:
567
+ return None
568
+ if len(value) == 0:
569
+ raise UsageError("Sweep ID cannot be empty")
570
+ if len(value) > len(value.strip()):
571
+ raise UsageError("Sweep ID cannot start or end with whitespace")
572
+ if not bool(value.strip()):
573
+ raise UsageError("Sweep ID cannot contain only whitespace")
574
+ return value
1079
575
 
1080
- @staticmethod
1081
- def _validate_base_url(value: Optional[str]) -> bool:
1082
- """Validate the base url of the wandb server.
576
+ # Computed fields.
1083
577
 
1084
- param value: URL to validate
578
+ @computed_field # type: ignore[prop-decorator]
579
+ @property
580
+ def _args(self) -> list[str]:
581
+ if not self._jupyter:
582
+ return sys.argv[1:]
583
+ return []
1085
584
 
1086
- Based on the Django URLValidator, but with a few additional checks.
585
+ @computed_field # type: ignore[prop-decorator]
586
+ @property
587
+ def _aws_lambda(self) -> bool:
588
+ """Check if we are running in a lambda environment."""
589
+ from sentry_sdk.integrations.aws_lambda import ( # type: ignore[import-not-found]
590
+ get_lambda_bootstrap,
591
+ )
1087
592
 
1088
- Copyright (c) Django Software Foundation and individual contributors.
1089
- All rights reserved.
593
+ lambda_bootstrap = get_lambda_bootstrap()
594
+ if not lambda_bootstrap or not hasattr(
595
+ lambda_bootstrap, "handle_event_request"
596
+ ):
597
+ return False
598
+ return True
1090
599
 
1091
- Redistribution and use in source and binary forms, with or without modification,
1092
- are permitted provided that the following conditions are met:
600
+ @computed_field # type: ignore[prop-decorator]
601
+ @property
602
+ def _code_path_local(self) -> str | None:
603
+ """The relative path from the current working directory to the code path.
1093
604
 
1094
- 1. Redistributions of source code must retain the above copyright notice,
1095
- this list of conditions and the following disclaimer.
605
+ For example, if the code path is /home/user/project/example.py, and the
606
+ current working directory is /home/user/project, then the code path local
607
+ is example.py.
1096
608
 
1097
- 2. Redistributions in binary form must reproduce the above copyright
1098
- notice, this list of conditions and the following disclaimer in the
1099
- documentation and/or other materials provided with the distribution.
609
+ If couldn't find the relative path, this will be an empty string.
610
+ """
611
+ return self._get_program_relpath(self.program) if self.program else None
1100
612
 
1101
- 3. Neither the name of Django nor the names of its contributors may be used
1102
- to endorse or promote products derived from this software without
1103
- specific prior written permission.
613
+ @computed_field # type: ignore[prop-decorator]
614
+ @property
615
+ def _colab(self) -> bool:
616
+ return "google.colab" in sys.modules
1104
617
 
1105
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1106
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1107
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1108
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
1109
- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
1110
- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
1111
- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
1112
- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1113
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1114
- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1115
- """
1116
- if value is None:
1117
- return True
618
+ @computed_field # type: ignore[prop-decorator]
619
+ @property
620
+ def _ipython(self) -> bool:
621
+ return ipython.in_ipython()
1118
622
 
1119
- ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
623
+ @computed_field # type: ignore[prop-decorator]
624
+ @property
625
+ def _jupyter(self) -> bool:
626
+ return ipython.in_jupyter()
1120
627
 
1121
- # IP patterns
1122
- ipv4_re = (
1123
- r"(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)"
1124
- r"(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}"
1125
- )
1126
- ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later)
628
+ @computed_field # type: ignore[prop-decorator]
629
+ @property
630
+ def _kaggle(self) -> bool:
631
+ return util._is_likely_kaggle()
1127
632
 
1128
- # Host patterns
1129
- hostname_re = (
1130
- r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
1131
- )
1132
- # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
1133
- domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
1134
- tld_re = (
1135
- r"\." # dot
1136
- r"(?!-)" # can't start with a dash
1137
- r"(?:[a-z" + ul + "-]{2,63}" # domain label
1138
- r"|xn--[a-z0-9]{1,59})" # or punycode label
1139
- r"(?<!-)" # can't end with a dash
1140
- r"\.?" # may have a trailing dot
1141
- )
1142
- # host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
1143
- # todo?: allow hostname to be just a hostname (no tld)?
1144
- host_re = "(" + hostname_re + domain_re + f"({tld_re})?" + "|localhost)"
1145
-
1146
- regex = re.compile(
1147
- r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
1148
- r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
1149
- r"(?:" + ipv4_re + "|" + ipv6_re + "|" + host_re + ")"
1150
- r"(?::[0-9]{1,5})?" # port
1151
- r"(?:[/?#][^\s]*)?" # resource path
1152
- r"\Z",
1153
- re.IGNORECASE,
1154
- )
1155
- schemes = {"http", "https"}
1156
- unsafe_chars = frozenset("\t\r\n")
633
+ @computed_field # type: ignore[prop-decorator]
634
+ @property
635
+ def _noop(self) -> bool:
636
+ return self.mode == "disabled"
1157
637
 
1158
- scheme = value.split("://")[0].lower()
1159
- split_url = urlsplit(value)
1160
- parsed_url = urlparse(value)
638
+ @computed_field # type: ignore[prop-decorator]
639
+ @property
640
+ def _notebook(self) -> bool:
641
+ return self._ipython or self._jupyter or self._colab or self._kaggle
1161
642
 
1162
- if re.match(r".*wandb\.ai[^\.]*$", value) and "api." not in value:
1163
- # user might guess app.wandb.ai or wandb.ai is the default cloud server
1164
- raise UsageError(
1165
- f"{value} is not a valid server address, did you mean https://api.wandb.ai?"
1166
- )
1167
- elif re.match(r".*wandb\.ai[^\.]*$", value) and scheme != "https":
1168
- raise UsageError("http is not secure, please use https://api.wandb.ai")
1169
- elif parsed_url.netloc == "":
1170
- raise UsageError(f"Invalid URL: {value}")
1171
- elif unsafe_chars.intersection(value):
1172
- raise UsageError("URL cannot contain unsafe characters")
1173
- elif scheme not in schemes:
1174
- raise UsageError("URL must start with `http(s)://`")
1175
- elif not regex.search(value):
1176
- raise UsageError(f"{value} is not a valid server address")
1177
- elif split_url.hostname is None or len(split_url.hostname) > 253:
1178
- raise UsageError("hostname is invalid")
643
+ @computed_field # type: ignore[prop-decorator]
644
+ @property
645
+ def _offline(self) -> bool:
646
+ return self.mode in ("offline", "dryrun")
1179
647
 
1180
- return True
648
+ @computed_field # type: ignore[prop-decorator]
649
+ @property
650
+ def _os(self) -> str:
651
+ """The operating system of the machine running the script."""
652
+ return platform.platform(aliased=True)
1181
653
 
1182
- @staticmethod
1183
- def _process_disable_service(value: Union[str, bool]) -> bool:
1184
- value = _str_as_bool(value)
1185
- if value:
1186
- wandb.termwarn(
1187
- "Disabling the wandb service is deprecated as of version 0.18.0 and will be removed in future versions. ",
1188
- repeat=False,
1189
- )
1190
- return value
654
+ @computed_field # type: ignore[prop-decorator]
655
+ @property
656
+ def _platform(self) -> str:
657
+ return f"{platform.system()}-{platform.machine()}".lower()
1191
658
 
1192
- @staticmethod
1193
- def _validate__service_wait(value: float) -> bool:
1194
- if value <= 0:
1195
- raise UsageError("_service_wait must be a positive number")
1196
- return True
659
+ @computed_field # type: ignore[prop-decorator]
660
+ @property
661
+ def _python(self) -> str:
662
+ return f"{platform.python_implementation()} {platform.python_version()}"
1197
663
 
1198
- @staticmethod
1199
- def _validate__stats_sampling_interval(value: float) -> bool:
1200
- if value < 0.1:
1201
- raise UsageError("sampling interval must be >= 0.1 seconds")
1202
- return True
664
+ @computed_field # type: ignore[prop-decorator]
665
+ @property
666
+ def _shared(self) -> bool:
667
+ """Whether we are in shared mode.
1203
668
 
1204
- @staticmethod
1205
- def _validate__stats_sample_rate_seconds(value: float) -> bool:
1206
- if value < 0.1:
1207
- raise UsageError("_stats_sample_rate_seconds must be >= 0.1")
1208
- return True
669
+ In "shared" mode, multiple processes can write to the same run,
670
+ for example from different machines.
671
+ """
672
+ return self.mode == "shared"
1209
673
 
1210
- @staticmethod
1211
- def _validate__stats_samples_to_average(value: int) -> bool:
1212
- if value < 1 or value > 30:
1213
- raise UsageError("_stats_samples_to_average must be between 1 and 30")
1214
- return True
674
+ @computed_field # type: ignore[prop-decorator]
675
+ @property
676
+ def _start_datetime(self) -> str:
677
+ if self.x_start_time is None:
678
+ return ""
679
+ datetime_now = datetime.fromtimestamp(self.x_start_time)
680
+ return datetime_now.strftime("%Y%m%d_%H%M%S")
1215
681
 
1216
- @staticmethod
1217
- def _validate_job_source(value: str) -> bool:
1218
- valid_sources = ["repo", "artifact", "image"]
1219
- if value not in valid_sources:
1220
- raise UsageError(
1221
- f"Settings field `job_source`: {value!r} not in {valid_sources}"
1222
- )
1223
- return True
682
+ @computed_field # type: ignore[prop-decorator]
683
+ @property
684
+ def _tmp_code_dir(self) -> str:
685
+ return _path_convert(
686
+ self.wandb_dir,
687
+ f"{self.run_mode}-{self.timespec}-{self.run_id}",
688
+ "tmp",
689
+ "code",
690
+ )
1224
691
 
1225
- # other helper methods
1226
- @staticmethod
1227
- def _path_convert(*args: str) -> str:
1228
- """Join path and apply os.path.expanduser to it."""
1229
- return os.path.expanduser(os.path.join(*args))
1230
-
1231
- def _convert_console(self, console: str) -> str:
1232
- if console == "auto":
1233
- if (
1234
- self._jupyter
1235
- or (self.start_method == "thread")
1236
- or not self._disable_service
1237
- or self._windows
1238
- ):
1239
- console = "wrap"
1240
- else:
1241
- console = "redirect"
1242
- return console
692
+ @computed_field # type: ignore[prop-decorator]
693
+ @property
694
+ def _windows(self) -> bool:
695
+ return platform.system() == "Windows"
1243
696
 
1244
- def _get_colab_url(self) -> Optional[str]:
697
+ @computed_field # type: ignore[prop-decorator]
698
+ @property
699
+ def colab_url(self) -> str | None:
700
+ """The URL to the Colab notebook, if running in Colab."""
1245
701
  if not self._colab:
1246
702
  return None
1247
- if self._jupyter_path and self._jupyter_path.startswith("fileId="):
1248
- unescaped = unquote(self._jupyter_path)
703
+ if self.x_jupyter_path and self.x_jupyter_path.startswith("fileId="):
704
+ unescaped = unquote(self.x_jupyter_path)
1249
705
  return "https://colab.research.google.com/notebook#" + unescaped
1250
706
  return None
1251
707
 
1252
- def _get_program(self, program: Optional[str]) -> Optional[str]:
1253
- if program is not None and program != "<python with no main file>":
1254
- return program
1255
-
1256
- if not self._jupyter:
1257
- return program
1258
-
1259
- if self.notebook_name:
1260
- return self.notebook_name
708
+ @computed_field # type: ignore[prop-decorator]
709
+ @property
710
+ def deployment(self) -> Literal["local", "cloud"]:
711
+ return "local" if self.is_local else "cloud"
1261
712
 
1262
- if not self._jupyter_path:
1263
- return program
713
+ @computed_field # type: ignore[prop-decorator]
714
+ @property
715
+ def files_dir(self) -> str:
716
+ """Absolute path to the local directory where the run's files are stored."""
717
+ return self.x_files_dir or _path_convert(
718
+ self.wandb_dir,
719
+ f"{self.run_mode}-{self.timespec}-{self.run_id}",
720
+ "files",
721
+ )
1264
722
 
1265
- if self._jupyter_path.startswith("fileId="):
1266
- return self._jupyter_name
1267
- else:
1268
- return self._jupyter_path
723
+ @computed_field # type: ignore[prop-decorator]
724
+ @property
725
+ def is_local(self) -> bool:
726
+ return str(self.base_url) != "https://api.wandb.ai"
1269
727
 
1270
- def _get_url_query_string(self) -> str:
1271
- # TODO(settings) use `wandb_setting` (if self.anonymous != "true":)
1272
- if Api().settings().get("anonymous") != "true":
1273
- return ""
728
+ @computed_field # type: ignore[prop-decorator]
729
+ @property
730
+ def log_dir(self) -> str:
731
+ """The directory for storing log files."""
732
+ return _path_convert(
733
+ self.wandb_dir, f"{self.run_mode}-{self.timespec}-{self.run_id}", "logs"
734
+ )
1274
735
 
1275
- api_key = apikey.api_key(settings=self)
736
+ @computed_field # type: ignore[prop-decorator]
737
+ @property
738
+ def log_internal(self) -> str:
739
+ """The path to the file to use for internal logs."""
740
+ return _path_convert(self.log_dir, "debug-internal.log")
1276
741
 
1277
- return f"?{urlencode({'apiKey': api_key})}"
742
+ @computed_field # type: ignore[prop-decorator]
743
+ @property
744
+ def log_symlink_internal(self) -> str:
745
+ """The path to the symlink to the internal log file of the most recent run."""
746
+ return _path_convert(self.wandb_dir, "debug-internal.log")
1278
747
 
1279
- def _project_url_base(self) -> str:
1280
- if not all([self.entity, self.project]):
1281
- return ""
748
+ @computed_field # type: ignore[prop-decorator]
749
+ @property
750
+ def log_symlink_user(self) -> str:
751
+ """The path to the symlink to the user-process log file of the most recent run."""
752
+ return _path_convert(self.wandb_dir, "debug.log")
1282
753
 
1283
- app_url = wandb.util.app_url(self.base_url)
1284
- return f"{app_url}/{quote(self.entity)}/{quote(self.project)}"
754
+ @computed_field # type: ignore[prop-decorator]
755
+ @property
756
+ def log_user(self) -> str:
757
+ """The path to the file to use for user-process logs."""
758
+ return _path_convert(self.log_dir, "debug.log")
1285
759
 
1286
- def _project_url(self) -> str:
760
+ @computed_field # type: ignore[prop-decorator]
761
+ @property
762
+ def project_url(self) -> str:
763
+ """The W&B URL where the project can be viewed."""
1287
764
  project_url = self._project_url_base()
1288
765
  if not project_url:
1289
766
  return ""
@@ -1292,401 +769,133 @@ class Settings(SettingsData):
1292
769
 
1293
770
  return f"{project_url}{query}"
1294
771
 
1295
- def _run_url(self) -> str:
1296
- """Return the run url."""
772
+ @computed_field # type: ignore[prop-decorator]
773
+ @property
774
+ def resume_fname(self) -> str:
775
+ """The path to the resume file."""
776
+ return _path_convert(self.wandb_dir, "wandb-resume.json")
777
+
778
+ @computed_field # type: ignore[prop-decorator]
779
+ @property
780
+ def run_mode(self) -> Literal["run", "offline-run"]:
781
+ return "run" if not self._offline else "offline-run"
782
+
783
+ @computed_field # type: ignore[prop-decorator]
784
+ @property
785
+ def run_url(self) -> str:
786
+ """The W&B URL where the run can be viewed."""
1297
787
  project_url = self._project_url_base()
1298
788
  if not all([project_url, self.run_id]):
1299
789
  return ""
1300
790
 
1301
791
  query = self._get_url_query_string()
1302
- return f"{project_url}/runs/{quote(self.run_id)}{query}"
792
+ return f"{project_url}/runs/{quote(self.run_id or '')}{query}"
1303
793
 
1304
- def _set_run_start_time(self, source: int = Source.BASE) -> None:
1305
- """Set the time stamps for the settings.
1306
-
1307
- Called once the run is initialized.
1308
- """
1309
- time_stamp: float = time.time()
1310
- datetime_now: datetime = datetime.fromtimestamp(time_stamp)
1311
- datetime_now_str = _datetime_as_str(datetime_now)
1312
- object.__setattr__(self, "_Settings_start_datetime", datetime_now_str)
1313
- object.__setattr__(self, "_Settings_start_time", time_stamp)
1314
- self.update(
1315
- _start_datetime=datetime_now_str,
1316
- _start_time=time_stamp,
1317
- source=source,
1318
- )
794
+ @computed_field # type: ignore[prop-decorator]
795
+ @property
796
+ def settings_workspace(self) -> str:
797
+ """The path to the workspace settings file."""
798
+ return _path_convert(self.wandb_dir, "settings")
1319
799
 
1320
- def _sweep_url(self) -> str:
1321
- """Return the sweep url."""
800
+ @computed_field # type: ignore[prop-decorator]
801
+ @property
802
+ def sweep_url(self) -> str:
803
+ """The W&B URL where the sweep can be viewed."""
1322
804
  project_url = self._project_url_base()
1323
805
  if not all([project_url, self.sweep_id]):
1324
806
  return ""
1325
807
 
1326
808
  query = self._get_url_query_string()
1327
- return f"{project_url}/sweeps/{quote(self.sweep_id)}{query}"
1328
-
1329
- def __init__(self, **kwargs: Any) -> None:
1330
- self.__frozen: bool = False
1331
- self.__initialized: bool = False
1332
-
1333
- self.__modification_order = SETTINGS_TOPOLOGICALLY_SORTED
1334
-
1335
- # Set default settings values
1336
- # We start off with the class attributes and `default_props` dicts
1337
- # and then create Property objects.
1338
- # Once initialized, attributes are to only be updated using the `update` method
1339
- default_props = self._default_props()
1340
-
1341
- # Init instance attributes as Property objects.
1342
- # Type hints of class attributes are used to generate a type validator function
1343
- # for runtime checks for each attribute.
1344
- # These are defaults, using Source.BASE for non-policy attributes and Source.RUN for policies.
1345
- for prop, type_hint in get_type_hints(SettingsData).items():
1346
- validators = [self._validator_factory(type_hint)]
1347
-
1348
- if prop in default_props:
1349
- validator = default_props[prop].pop("validator", [])
1350
- # Property validator could be either Callable or Sequence[Callable]
1351
- if callable(validator):
1352
- validators.append(validator)
1353
- elif isinstance(validator, Sequence):
1354
- validators.extend(list(validator))
1355
- object.__setattr__(
1356
- self,
1357
- prop,
1358
- Property(
1359
- name=prop,
1360
- **default_props[prop],
1361
- validator=validators,
1362
- # todo: double-check this logic:
1363
- source=Source.RUN
1364
- if default_props[prop].get("is_policy", False)
1365
- else Source.BASE,
1366
- ),
1367
- )
1368
- else:
1369
- object.__setattr__(
1370
- self,
1371
- prop,
1372
- Property(
1373
- name=prop,
1374
- validator=validators,
1375
- source=Source.BASE,
1376
- ),
1377
- )
809
+ return f"{project_url}/sweeps/{quote(self.sweep_id or '')}{query}"
1378
810
 
1379
- # update overridden defaults from kwargs
1380
- unexpected_arguments = [k for k in kwargs.keys() if k not in self.__dict__]
1381
- # allow only explicitly defined arguments
1382
- if unexpected_arguments:
1383
- raise SettingsUnexpectedArgsError(
1384
- f"Got unexpected arguments: {unexpected_arguments}. "
1385
- )
811
+ @computed_field # type: ignore[prop-decorator]
812
+ @property
813
+ def sync_dir(self) -> str:
814
+ return _path_convert(
815
+ self.wandb_dir, f"{self.run_mode}-{self.timespec}-{self.run_id}"
816
+ )
1386
817
 
1387
- # automatically inspect setting validators and runtime hooks and topologically sort them
1388
- # so that we can safely update them. throw error if there are cycles.
1389
- for prop in self.__modification_order:
1390
- if prop in kwargs:
1391
- source = Source.RUN if self.__dict__[prop].is_policy else Source.BASE
1392
- self.update({prop: kwargs[prop]}, source=source)
1393
- kwargs.pop(prop)
1394
-
1395
- for k, v in kwargs.items():
1396
- # todo: double-check this logic:
1397
- source = Source.RUN if self.__dict__[k].is_policy else Source.BASE
1398
- self.update({k: v}, source=source)
1399
-
1400
- # setup private attributes
1401
- object.__setattr__(self, "_Settings_start_datetime", None)
1402
- object.__setattr__(self, "_Settings_start_time", None)
1403
-
1404
- # done with init, use self.update() to update attributes from now on
1405
- self.__initialized = True
1406
-
1407
- # todo? freeze settings to prevent accidental changes
1408
- # self.freeze()
1409
-
1410
- def __str__(self) -> str:
1411
- # get attributes that are instances of the Property class:
1412
- representation = {
1413
- k: v.value for k, v in self.__dict__.items() if isinstance(v, Property)
1414
- }
1415
- return f"<Settings {_redact_dict(representation)}>"
1416
-
1417
- def __repr__(self) -> str:
1418
- # private attributes
1419
- private = {k: v for k, v in self.__dict__.items() if k.startswith("_Settings")}
1420
- # get attributes that are instances of the Property class:
1421
- attributes = {
1422
- k: f"<Property value={v.value} source={v.source}>"
1423
- for k, v in self.__dict__.items()
1424
- if isinstance(v, Property)
1425
- }
1426
- representation = {**private, **attributes}
1427
- return f"<Settings {representation}>"
818
+ @computed_field # type: ignore[prop-decorator]
819
+ @property
820
+ def sync_file(self) -> str:
821
+ """Path to the append-only binary transaction log file."""
822
+ return _path_convert(self.sync_dir, f"run-{self.run_id}.wandb")
1428
823
 
1429
- def __copy__(self) -> "Settings":
1430
- """Ensure that a copy of the settings object is a truly deep copy.
824
+ @computed_field # type: ignore[prop-decorator]
825
+ @property
826
+ def sync_symlink_latest(self) -> str:
827
+ return _path_convert(self.wandb_dir, "latest-run")
1431
828
 
1432
- Note that the copied object will not be frozen todo? why is this needed?
1433
- """
1434
- # get attributes that are instances of the Property class:
1435
- attributes = {k: v for k, v in self.__dict__.items() if isinstance(v, Property)}
1436
- new = Settings()
1437
- # update properties that have deps or are dependent on in the topologically-sorted order
1438
- for prop in self.__modification_order:
1439
- new.update({prop: attributes[prop]._value}, source=attributes[prop].source)
1440
- attributes.pop(prop)
1441
-
1442
- # update the remaining attributes
1443
- for k, v in attributes.items():
1444
- # make sure to use the raw property value (v._value),
1445
- # not the potential result of runtime hooks applied to it (v.value)
1446
- new.update({k: v._value}, source=v.source)
1447
- new.unfreeze()
1448
-
1449
- return new
1450
-
1451
- def __deepcopy__(self, memo: dict) -> "Settings":
1452
- return self.__copy__()
1453
-
1454
- # attribute access methods
1455
- @no_type_check # this is a hack to make mypy happy
1456
- def __getattribute__(self, name: str) -> Any:
1457
- """Expose `attribute.value` if `attribute` is a Property."""
1458
- item = object.__getattribute__(self, name)
1459
- if isinstance(item, Property):
1460
- return item.value
1461
- return item
1462
-
1463
- def __setattr__(self, key: str, value: Any) -> None:
1464
- if "_Settings__initialized" in self.__dict__ and self.__initialized:
1465
- raise TypeError(f"Please use update() to update attribute `{key}` value")
1466
- object.__setattr__(self, key, value)
1467
-
1468
- def __iter__(self) -> Iterable:
1469
- return iter(self.to_dict())
1470
-
1471
- def copy(self) -> "Settings":
1472
- return self.__copy__()
1473
-
1474
- # implement the Mapping interface
1475
- def keys(self) -> Iterable[str]:
1476
- return self.to_dict().keys()
1477
-
1478
- @no_type_check # this is a hack to make mypy happy
1479
- def __getitem__(self, name: str) -> Any:
1480
- """Expose attribute.value if attribute is a Property."""
1481
- item = object.__getattribute__(self, name)
1482
- if isinstance(item, Property):
1483
- return item.value
1484
- return item
1485
-
1486
- def update(
1487
- self,
1488
- settings: Optional[Union[Dict[str, Any], "Settings"]] = None,
1489
- source: int = Source.OVERRIDE,
1490
- **kwargs: Any,
1491
- ) -> None:
1492
- """Update individual settings."""
1493
- if "_Settings__frozen" in self.__dict__ and self.__frozen:
1494
- raise TypeError("Settings object is frozen")
1495
-
1496
- if isinstance(settings, Settings):
1497
- # If a Settings object is passed, detect the settings that differ
1498
- # from defaults, collect them into a dict, and apply them using `source`.
1499
- # This comes up in `wandb.init(settings=wandb.Settings(...))` and
1500
- # seems like the behavior that the user would expect when calling init that way.
1501
- defaults = Settings()
1502
- settings_dict = dict()
1503
- for k, v in settings.__dict__.items():
1504
- if isinstance(v, Property):
1505
- if v._value != defaults.__dict__[k]._value:
1506
- settings_dict[k] = v._value
1507
- # replace with the generated dict
1508
- settings = settings_dict
1509
-
1510
- # add kwargs to settings
1511
- settings = settings or dict()
1512
- # explicit kwargs take precedence over settings
1513
- settings = {**settings, **kwargs}
1514
- unknown_properties = []
1515
- for key in settings.keys():
1516
- # only allow updating known Properties
1517
- if key not in self.__dict__ or not isinstance(self.__dict__[key], Property):
1518
- unknown_properties.append(key)
1519
- if unknown_properties:
1520
- raise KeyError(f"Unknown settings: {unknown_properties}")
1521
- # only if all keys are valid, update them
1522
-
1523
- # store settings to be updated in a dict to preserve stats on preprocessing and validation errors
1524
- settings.copy()
1525
-
1526
- # update properties that have deps or are dependent on in the topologically-sorted order
1527
- for key in self.__modification_order:
1528
- if key in settings:
1529
- self.__dict__[key].update(settings.pop(key), source=source)
1530
-
1531
- # update the remaining properties
1532
- for key, value in settings.items():
1533
- self.__dict__[key].update(value, source)
1534
-
1535
- def items(self) -> ItemsView[str, Any]:
1536
- return self.to_dict().items()
1537
-
1538
- def get(self, key: str, default: Optional[Any] = None) -> Any:
1539
- return self.to_dict().get(key, default)
1540
-
1541
- def freeze(self) -> None:
1542
- object.__setattr__(self, "_Settings__frozen", True)
1543
-
1544
- def unfreeze(self) -> None:
1545
- object.__setattr__(self, "_Settings__frozen", False)
1546
-
1547
- def is_frozen(self) -> bool:
1548
- return self.__frozen
1549
-
1550
- def to_dict(self) -> Dict[str, Any]:
1551
- """Return a dict representation of the settings."""
1552
- # get attributes that are instances of the Property class:
1553
- attributes = {
1554
- k: v.value for k, v in self.__dict__.items() if isinstance(v, Property)
1555
- }
1556
- return attributes
829
+ @computed_field # type: ignore[prop-decorator]
830
+ @property
831
+ def timespec(self) -> str:
832
+ return self._start_datetime
1557
833
 
1558
- def to_proto(self) -> wandb_settings_pb2.Settings:
1559
- """Generate a protobuf representation of the settings."""
1560
- from dataclasses import fields
1561
-
1562
- settings = wandb_settings_pb2.Settings()
1563
- for field in fields(SettingsData):
1564
- k = field.name
1565
- v = getattr(self, k)
1566
- # special case for _stats_open_metrics_filters
1567
- if k == "_stats_open_metrics_filters":
1568
- if isinstance(v, (list, set, tuple)):
1569
- setting = getattr(settings, k)
1570
- setting.sequence.value.extend(v)
1571
- elif isinstance(v, dict):
1572
- setting = getattr(settings, k)
1573
- for key, value in v.items():
1574
- for kk, vv in value.items():
1575
- setting.mapping.value[key].value[kk] = vv
1576
- else:
1577
- raise TypeError(f"Unsupported type {type(v)} for setting {k}")
1578
- continue
834
+ @computed_field # type: ignore[prop-decorator]
835
+ @property
836
+ def wandb_dir(self) -> str:
837
+ """Full path to the wandb directory.
1579
838
 
1580
- if isinstance(v, bool):
1581
- getattr(settings, k).CopyFrom(BoolValue(value=v))
1582
- elif isinstance(v, int):
1583
- getattr(settings, k).CopyFrom(Int32Value(value=v))
1584
- elif isinstance(v, float):
1585
- getattr(settings, k).CopyFrom(DoubleValue(value=v))
1586
- elif isinstance(v, str):
1587
- getattr(settings, k).CopyFrom(StringValue(value=v))
1588
- elif isinstance(v, (list, set, tuple)):
1589
- # we only support sequences of strings for now
1590
- sequence = getattr(settings, k)
1591
- sequence.value.extend(v)
1592
- elif isinstance(v, dict):
1593
- mapping = getattr(settings, k)
1594
- for key, value in v.items():
1595
- # we only support dicts with string values for now
1596
- mapping.value[key] = value
1597
- elif isinstance(v, RunMoment):
1598
- getattr(settings, k).CopyFrom(
1599
- wandb_settings_pb2.RunMoment(
1600
- run=v.run,
1601
- value=v.value,
1602
- metric=v.metric,
1603
- )
1604
- )
1605
- elif v is None:
1606
- # None is the default value for all settings, so we don't need to set it,
1607
- # i.e. None means that the value was not set.
1608
- pass
1609
- else:
1610
- raise TypeError(f"Unsupported type {type(v)} for setting {k}")
1611
- # TODO: store property sources in the protobuf so that we can reconstruct the
1612
- # settings object from the protobuf
1613
- return settings
1614
-
1615
- # apply settings from different sources
1616
- # TODO(dd): think about doing some|all of that at init
1617
- def _apply_settings(
1618
- self,
1619
- settings: "Settings",
1620
- _logger: Optional[_EarlyLogger] = None,
1621
- ) -> None:
1622
- """Apply settings from a Settings object."""
1623
- if _logger is not None:
1624
- _logger.info(f"Applying settings from {settings}")
1625
- attributes = {
1626
- k: v for k, v in settings.__dict__.items() if isinstance(v, Property)
1627
- }
1628
- # update properties that have deps or are dependent on in the topologically-sorted order
1629
- for prop in self.__modification_order:
1630
- self.update({prop: attributes[prop]._value}, source=attributes[prop].source)
1631
- attributes.pop(prop)
1632
- # update the remaining properties
1633
- for k, v in attributes.items():
1634
- # note that only the same/higher priority settings are propagated
1635
- self.update({k: v._value}, source=v.source)
839
+ The setting exposed to users as `dir=` or `WANDB_DIR` is the `root_dir`.
840
+ We add the `__stage_dir__` to it to get the full `wandb_dir`
841
+ """
842
+ root_dir = self.root_dir or ""
1636
843
 
1637
- @staticmethod
1638
- def _load_config_file(file_name: str, section: str = "default") -> dict:
1639
- parser = configparser.ConfigParser()
1640
- parser.add_section(section)
1641
- parser.read(file_name)
1642
- config: Dict[str, Any] = dict()
1643
- for k in parser[section]:
1644
- config[k] = parser[section][k]
1645
- # TODO (cvp): we didn't do this in the old cli, but it seems necessary
1646
- if k == "ignore_globs":
1647
- config[k] = config[k].split(",")
1648
- return config
844
+ # We use the hidden version if it already exists, otherwise non-hidden.
845
+ if os.path.exists(os.path.join(root_dir, ".wandb")):
846
+ __stage_dir__ = ".wandb" + os.sep
847
+ else:
848
+ __stage_dir__ = "wandb" + os.sep
1649
849
 
1650
- def _apply_base(self, pid: int, _logger: Optional[_EarlyLogger] = None) -> None:
1651
- if _logger is not None:
1652
- _logger.info(f"Current SDK version is {wandb.__version__}")
1653
- _logger.info(f"Configure stats pid to {pid}")
1654
- self.update({"_stats_pid": pid}, source=Source.SETUP)
1655
-
1656
- def _apply_config_files(self, _logger: Optional[_EarlyLogger] = None) -> None:
1657
- # TODO(jhr): permit setting of config in system and workspace
1658
- if self.settings_system is not None:
1659
- if _logger is not None:
1660
- _logger.info(f"Loading settings from {self.settings_system}")
1661
- self.update(
1662
- self._load_config_file(self.settings_system),
1663
- source=Source.SYSTEM,
850
+ path = os.path.join(root_dir, __stage_dir__)
851
+ if not os.access(root_dir or ".", os.W_OK):
852
+ termwarn(
853
+ f"Path {path} wasn't writable, using system temp directory.",
854
+ repeat=False,
1664
855
  )
1665
- if self.settings_workspace is not None:
1666
- if _logger is not None:
1667
- _logger.info(f"Loading settings from {self.settings_workspace}")
1668
- self.update(
1669
- self._load_config_file(self.settings_workspace),
1670
- source=Source.WORKSPACE,
856
+ path = os.path.join(
857
+ tempfile.gettempdir(), __stage_dir__ or ("wandb" + os.sep)
1671
858
  )
1672
859
 
1673
- def _apply_env_vars(
1674
- self,
1675
- environ: Mapping[str, Any],
1676
- _logger: Optional[_EarlyLogger] = None,
1677
- ) -> None:
860
+ return os.path.expanduser(path)
861
+
862
+ # Methods to collect and update settings from different sources.
863
+ #
864
+ # The Settings class does not track the source of the settings,
865
+ # so it is up to the developer to ensure that the settings are applied
866
+ # in the correct order. Most of the updates are done in
867
+ # wandb/sdk/wandb_setup.py::_WandbSetup__WandbSetup._settings_setup.
868
+
869
+ def update_from_system_config_file(self):
870
+ """Update settings from the system config file."""
871
+ if not self.settings_system or not os.path.exists(self.settings_system):
872
+ return
873
+ for key, value in self._load_config_file(self.settings_system).items():
874
+ if value is not None:
875
+ setattr(self, key, value)
876
+
877
+ def update_from_workspace_config_file(self):
878
+ """Update settings from the workspace config file."""
879
+ if not self.settings_workspace or not os.path.exists(self.settings_workspace):
880
+ return
881
+ for key, value in self._load_config_file(self.settings_workspace).items():
882
+ if value is not None:
883
+ setattr(self, key, value)
884
+
885
+ def update_from_env_vars(self, environ: dict[str, Any]):
886
+ """Update settings from environment variables."""
1678
887
  env_prefix: str = "WANDB_"
888
+ private_env_prefix: str = env_prefix + "_"
1679
889
  special_env_var_names = {
1680
- "WANDB_TRACELOG": "_tracelog",
1681
- "WANDB_DISABLE_SERVICE": "_disable_service",
1682
- "WANDB_SERVICE_TRANSPORT": "_service_transport",
890
+ "WANDB_DISABLE_SERVICE": "x_disable_service",
891
+ "WANDB_SERVICE_TRANSPORT": "x_service_transport",
1683
892
  "WANDB_DIR": "root_dir",
1684
893
  "WANDB_NAME": "run_name",
1685
894
  "WANDB_NOTES": "run_notes",
1686
895
  "WANDB_TAGS": "run_tags",
1687
896
  "WANDB_JOB_TYPE": "run_job_type",
1688
- "WANDB_HTTP_TIMEOUT": "_graphql_timeout_seconds",
1689
- "WANDB_FILE_PUSHER_TIMEOUT": "_file_transfer_timeout_seconds",
897
+ "WANDB_HTTP_TIMEOUT": "x_graphql_timeout_seconds",
898
+ "WANDB_FILE_PUSHER_TIMEOUT": "x_file_transfer_timeout_seconds",
1690
899
  "WANDB_USER_EMAIL": "email",
1691
900
  }
1692
901
  env = dict()
@@ -1696,6 +905,8 @@ class Settings(SettingsData):
1696
905
 
1697
906
  if setting in special_env_var_names:
1698
907
  key = special_env_var_names[setting]
908
+ elif setting.startswith(private_env_prefix):
909
+ key = "x_" + setting[len(private_env_prefix) :].lower()
1699
910
  else:
1700
911
  # otherwise, strip the prefix and convert to lowercase
1701
912
  key = setting[len(env_prefix) :].lower()
@@ -1704,52 +915,37 @@ class Settings(SettingsData):
1704
915
  if key in ("ignore_globs", "run_tags"):
1705
916
  value = value.split(",")
1706
917
  env[key] = value
1707
- elif _logger is not None:
1708
- _logger.warning(f"Unknown environment variable: {setting}")
1709
918
 
1710
- if _logger is not None:
1711
- _logger.info(
1712
- f"Loading settings from environment variables: {_redact_dict(env)}"
1713
- )
1714
- self.update(env, source=Source.ENV)
1715
-
1716
- def _infer_settings_from_environment(
1717
- self, _logger: Optional[_EarlyLogger] = None
1718
- ) -> None:
1719
- """Modify settings based on environment (for runs and cli)."""
1720
- settings: Dict[str, Union[bool, str, Sequence, None]] = dict()
1721
- # disable symlinks if on windows (requires admin or developer setup)
1722
- settings["symlink"] = True
1723
- if self._windows:
1724
- settings["symlink"] = False
1725
-
1726
- # TODO(jhr): this needs to be moved last in setting up settings ?
1727
- # (dd): loading order does not matter as long as source is set correctly
919
+ for key, value in env.items():
920
+ if value is not None:
921
+ setattr(self, key, value)
1728
922
 
923
+ def update_from_system_environment(self):
924
+ """Update settings from the system environment."""
1729
925
  # For code saving, only allow env var override if value from server is true, or
1730
926
  # if no preference was specified.
1731
927
  if (self.save_code is True or self.save_code is None) and (
1732
- os.getenv(wandb.env.SAVE_CODE) is not None
1733
- or os.getenv(wandb.env.DISABLE_CODE) is not None
928
+ os.getenv(env.SAVE_CODE) is not None
929
+ or os.getenv(env.DISABLE_CODE) is not None
1734
930
  ):
1735
- settings["save_code"] = wandb.env.should_save_code()
931
+ self.save_code = env.should_save_code()
1736
932
 
1737
- settings["disable_git"] = wandb.env.disable_git()
933
+ self.disable_git = env.disable_git()
1738
934
 
1739
935
  # Attempt to get notebook information if not already set by the user
1740
936
  if self._jupyter and (self.notebook_name is None or self.notebook_name == ""):
1741
937
  meta = wandb.jupyter.notebook_metadata(self.silent) # type: ignore
1742
- settings["_jupyter_path"] = meta.get("path")
1743
- settings["_jupyter_name"] = meta.get("name")
1744
- settings["_jupyter_root"] = meta.get("root")
938
+ self.x_jupyter_path = meta.get("path")
939
+ self.x_jupyter_name = meta.get("name")
940
+ self.x_jupyter_root = meta.get("root")
1745
941
  elif (
1746
942
  self._jupyter
1747
943
  and self.notebook_name is not None
1748
944
  and os.path.exists(self.notebook_name)
1749
945
  ):
1750
- settings["_jupyter_path"] = self.notebook_name
1751
- settings["_jupyter_name"] = self.notebook_name
1752
- settings["_jupyter_root"] = os.getcwd()
946
+ self.x_jupyter_path = self.notebook_name
947
+ self.x_jupyter_name = self.notebook_name
948
+ self.x_jupyter_root = os.getcwd()
1753
949
  elif self._jupyter:
1754
950
  wandb.termwarn(
1755
951
  "WANDB_NOTEBOOK_NAME should be a path to a notebook file, "
@@ -1759,244 +955,266 @@ class Settings(SettingsData):
1759
955
  # host and username are populated by apply_env_vars if corresponding env
1760
956
  # vars exist -- but if they don't, we'll fill them in here
1761
957
  if self.host is None:
1762
- settings["host"] = socket.gethostname() # type: ignore
958
+ self.host = socket.gethostname() # type: ignore
1763
959
 
1764
960
  if self.username is None:
1765
961
  try: # type: ignore
1766
- settings["username"] = getpass.getuser()
962
+ self.username = getpass.getuser()
1767
963
  except KeyError:
1768
964
  # getuser() could raise KeyError in restricted environments like
1769
965
  # chroot jails or docker containers. Return user id in these cases.
1770
- settings["username"] = str(os.getuid())
966
+ self.username = str(os.getuid())
1771
967
 
1772
968
  _executable = (
1773
- self._executable
1774
- or os.environ.get(wandb.env._EXECUTABLE)
969
+ self.x_executable
970
+ or os.environ.get(env._EXECUTABLE)
1775
971
  or sys.executable
1776
972
  or shutil.which("python3")
1777
973
  or "python3"
1778
974
  )
1779
- settings["_executable"] = _executable
975
+ self.x_executable = _executable
1780
976
 
1781
- settings["docker"] = wandb.env.get_docker(wandb.util.image_id_from_k8s())
977
+ self.docker = env.get_docker(util.image_id_from_k8s())
1782
978
 
1783
- # TODO: we should use the cuda library to collect this
1784
- if os.path.exists("/usr/local/cuda/version.txt"):
1785
- with open("/usr/local/cuda/version.txt") as f:
1786
- settings["_cuda"] = f.read().split(" ")[-1].strip()
1787
- if not self._jupyter:
1788
- settings["_args"] = sys.argv[1:]
1789
- settings["_os"] = platform.platform(aliased=True)
1790
- settings["_python"] = platform.python_version()
1791
-
1792
- if _logger is not None:
1793
- _logger.info(
1794
- f"Inferring settings from compute environment: {_redact_dict(settings)}"
1795
- )
979
+ # proceed if not in CLI mode
980
+ if self.x_cli_only_mode:
981
+ return
1796
982
 
1797
- self.update(settings, source=Source.ENV)
983
+ program = self.program or self._get_program()
1798
984
 
1799
- def _infer_run_settings_from_environment(
1800
- self,
1801
- _logger: Optional[_EarlyLogger] = None,
1802
- ) -> None:
1803
- """Modify settings based on environment (for runs only)."""
1804
- # If there's not already a program file, infer it now.
1805
- settings: Dict[str, Union[bool, str, None]] = dict()
1806
- program = self.program or _get_program()
1807
985
  if program is not None:
1808
986
  repo = GitRepo()
1809
987
  root = repo.root or os.getcwd()
1810
988
 
1811
- program_relpath = self.program_relpath or _get_program_relpath(
1812
- program, repo.root, _logger=_logger
989
+ self.program_relpath = self.program_relpath or self._get_program_relpath(
990
+ program, root
1813
991
  )
1814
- settings["program_relpath"] = program_relpath
1815
992
  program_abspath = os.path.abspath(
1816
993
  os.path.join(root, os.path.relpath(os.getcwd(), root), program)
1817
994
  )
1818
995
  if os.path.exists(program_abspath):
1819
- settings["program_abspath"] = program_abspath
996
+ self.program_abspath = program_abspath
1820
997
  else:
1821
998
  program = "<python with no main file>"
1822
999
 
1823
- settings["program"] = program
1000
+ self.program = program
1824
1001
 
1825
- if _logger is not None:
1826
- _logger.info(
1827
- f"Inferring run settings from compute environment: {_redact_dict(settings)}"
1828
- )
1002
+ def update_from_dict(self, settings: dict[str, Any]) -> None:
1003
+ """Update settings from a dictionary."""
1004
+ for key, value in dict(settings).items():
1005
+ if value is not None:
1006
+ setattr(self, key, value)
1829
1007
 
1830
- self.update(settings, source=Source.ENV)
1831
-
1832
- def _apply_setup(
1833
- self, setup_settings: Dict[str, Any], _logger: Optional[_EarlyLogger] = None
1834
- ) -> None:
1835
- if _logger:
1836
- _logger.info(f"Applying setup settings: {_redact_dict(setup_settings)}")
1837
- self.update(setup_settings, source=Source.SETUP)
1838
-
1839
- def _apply_user(
1840
- self, user_settings: Dict[str, Any], _logger: Optional[_EarlyLogger] = None
1841
- ) -> None:
1842
- if _logger:
1843
- _logger.info(f"Applying user settings: {_redact_dict(user_settings)}")
1844
- self.update(user_settings, source=Source.USER)
1845
-
1846
- def _apply_init(self, init_settings: Dict[str, Union[str, int, None]]) -> None:
1847
- # pop magic from init settings
1848
- init_settings.pop("magic", None)
1849
-
1850
- # prevent setting project, entity if in sweep
1851
- # TODO(jhr): these should be locked elements in the future
1852
- if self.sweep_id:
1853
- for key in ("project", "entity", "id"):
1854
- val = init_settings.pop(key, None)
1855
- if val:
1856
- wandb.termwarn(
1857
- f"Ignored wandb.init() arg {key} when running a sweep."
1858
- )
1859
- if self.launch:
1860
- if self.project is not None and init_settings.pop("project", None):
1861
- wandb.termwarn(
1862
- "Project is ignored when running from wandb launch context. "
1863
- "Ignored wandb.init() arg project when running running from launch.",
1864
- )
1865
- for key in ("entity", "id"):
1866
- # Init settings cannot override launch settings.
1867
- if init_settings.pop(key, None):
1868
- wandb.termwarn(
1869
- "Project, entity and id are ignored when running from wandb launch context. "
1870
- f"Ignored wandb.init() arg {key} when running running from launch.",
1871
- )
1008
+ def update_from_settings(self, settings: Settings) -> None:
1009
+ """Update settings from another instance of `Settings`."""
1010
+ d = {field: getattr(settings, field) for field in settings.model_fields_set}
1011
+ if d:
1012
+ self.update_from_dict(d)
1872
1013
 
1873
- # strip out items where value is None
1874
- param_map = dict(
1875
- name="run_name",
1876
- id="run_id",
1877
- tags="run_tags",
1878
- group="run_group",
1879
- job_type="run_job_type",
1880
- notes="run_notes",
1881
- dir="root_dir",
1882
- sweep_id="sweep_id",
1883
- )
1884
- init_settings = {
1885
- param_map.get(k, k): v for k, v in init_settings.items() if v is not None
1886
- }
1887
- # fun logic to convert the resume init arg
1888
- if init_settings.get("resume"):
1889
- if isinstance(init_settings["resume"], str):
1890
- if init_settings["resume"] not in ("allow", "must", "never", "auto"):
1891
- if init_settings.get("run_id") is None:
1892
- # TODO: deprecate or don't support
1893
- init_settings["run_id"] = init_settings["resume"]
1894
- init_settings["resume"] = "allow"
1895
- elif init_settings["resume"] is True:
1896
- # todo: add deprecation warning, switch to literal strings for resume
1897
- init_settings["resume"] = "auto"
1898
-
1899
- # update settings
1900
- self.update(init_settings, source=Source.INIT)
1901
- self._handle_fork_logic()
1902
- self._handle_rewind_logic()
1903
- self._handle_resume_logic()
1904
-
1905
- def _handle_fork_logic(self) -> None:
1906
- if self.fork_from is None:
1907
- return
1014
+ # Helper methods.
1908
1015
 
1909
- if self.run_id is not None and (self.fork_from.run == self.run_id):
1910
- raise ValueError(
1911
- "Provided `run_id` is the same as the run to `fork_from`. "
1912
- "Please provide a different `run_id` or remove the `run_id` argument. "
1913
- "If you want to rewind the current run, please use `resume_from` instead."
1914
- )
1016
+ def to_proto(self) -> wandb_settings_pb2.Settings:
1017
+ """Generate a protobuf representation of the settings."""
1018
+ settings_proto = wandb_settings_pb2.Settings()
1019
+ for k, v in self.model_dump(exclude_none=True).items():
1020
+ # special case for x_stats_open_metrics_filters
1021
+ if k == "x_stats_open_metrics_filters":
1022
+ if isinstance(v, (list, set, tuple)):
1023
+ setting = getattr(settings_proto, k)
1024
+ setting.sequence.value.extend(v)
1025
+ elif isinstance(v, dict):
1026
+ setting = getattr(settings_proto, k)
1027
+ for key, value in v.items():
1028
+ for kk, vv in value.items():
1029
+ setting.mapping.value[key].value[kk] = vv
1030
+ else:
1031
+ raise TypeError(f"Unsupported type {type(v)} for setting {k}")
1032
+ continue
1915
1033
 
1916
- def _handle_rewind_logic(self) -> None:
1917
- if self.resume_from is None:
1918
- return
1034
+ if isinstance(v, bool):
1035
+ getattr(settings_proto, k).CopyFrom(BoolValue(value=v))
1036
+ elif isinstance(v, int):
1037
+ getattr(settings_proto, k).CopyFrom(Int32Value(value=v))
1038
+ elif isinstance(v, float):
1039
+ getattr(settings_proto, k).CopyFrom(DoubleValue(value=v))
1040
+ elif isinstance(v, str):
1041
+ getattr(settings_proto, k).CopyFrom(StringValue(value=v))
1042
+ elif isinstance(v, (list, set, tuple)):
1043
+ # we only support sequences of strings for now
1044
+ sequence = getattr(settings_proto, k)
1045
+ sequence.value.extend(v)
1046
+ elif isinstance(v, dict):
1047
+ mapping = getattr(settings_proto, k)
1048
+ for key, value in v.items():
1049
+ # we only support dicts with string values for now
1050
+ mapping.value[key] = value
1051
+ elif isinstance(v, RunMoment):
1052
+ getattr(settings_proto, k).CopyFrom(
1053
+ wandb_settings_pb2.RunMoment(
1054
+ run=v.run,
1055
+ value=v.value,
1056
+ metric=v.metric,
1057
+ )
1058
+ )
1059
+ elif v is None:
1060
+ # None means that the setting value was not set.
1061
+ pass
1062
+ else:
1063
+ raise TypeError(f"Unsupported type {type(v)} for setting {k}")
1919
1064
 
1920
- if self.run_id is not None and (self.resume_from.run != self.run_id):
1921
- wandb.termwarn(
1922
- "Both `run_id` and `resume_from` have been specified with different ids. "
1923
- "`run_id` will be ignored."
1924
- )
1925
- self.update({"run_id": self.resume_from.run}, source=Source.INIT)
1065
+ return settings_proto
1926
1066
 
1927
- def _handle_resume_logic(self) -> None:
1067
+ def handle_resume_logic(self):
1068
+ """Handle logic for resuming runs."""
1928
1069
  # handle auto resume logic
1929
1070
  if self.resume == "auto":
1930
1071
  if os.path.exists(self.resume_fname):
1931
1072
  with open(self.resume_fname) as f:
1932
1073
  resume_run_id = json.load(f)["run_id"]
1933
1074
  if self.run_id is None:
1934
- self.update({"run_id": resume_run_id}, source=Source.INIT) # type: ignore
1075
+ self.run_id = resume_run_id
1935
1076
  elif self.run_id != resume_run_id:
1936
1077
  wandb.termwarn(
1937
1078
  "Tried to auto resume run with "
1938
1079
  f"id {resume_run_id} but id {self.run_id} is set.",
1939
1080
  )
1081
+ if self.run_id is None:
1082
+ self.run_id = generate_id()
1940
1083
 
1941
- self.update({"run_id": self.run_id or generate_id()}, source=Source.INIT)
1942
- # persist our run id in case of failure
1943
- # check None for mypy
1084
+ # persist run_id in case of failure
1944
1085
  if self.resume == "auto" and self.resume_fname is not None:
1945
1086
  filesystem.mkdir_exists_ok(self.wandb_dir)
1946
1087
  with open(self.resume_fname, "w") as f:
1947
1088
  f.write(json.dumps({"run_id": self.run_id}))
1948
1089
 
1949
- def _apply_login(
1950
- self,
1951
- login_settings: Dict[str, Any],
1952
- _logger: Optional[_EarlyLogger] = None,
1953
- ) -> None:
1954
- key_map = {
1955
- "key": "api_key",
1956
- "host": "base_url",
1957
- "timeout": "login_timeout",
1958
- }
1090
+ def handle_sweep_logic(self):
1091
+ """Update settings based on sweep context.
1959
1092
 
1960
- # Rename keys and keep only the non-None values.
1961
- #
1962
- # The input keys are parameters to wandb.login(), but we use different
1963
- # names for some of them in Settings.
1964
- login_settings = {
1965
- key_map.get(key, key): value
1966
- for key, value in login_settings.items()
1967
- if value is not None
1968
- }
1093
+ When running a sweep, the project, entity, and run_id are handled externally,
1094
+ and should be ignored if they are set.
1095
+ """
1096
+ if self.sweep_id is None:
1097
+ return
1098
+
1099
+ for key in ("project", "entity", "run_id"):
1100
+ value = getattr(self, key)
1101
+ if value is not None:
1102
+ wandb.termwarn(f"Ignoring {key} {value!r} when running a sweep.")
1103
+ setattr(self, key, None)
1969
1104
 
1970
- if _logger:
1971
- _logger.info(f"Applying login settings: {_redact_dict(login_settings)}")
1105
+ def handle_launch_logic(self):
1106
+ """Update settings based on launch context.
1972
1107
 
1973
- self.update(
1974
- login_settings,
1975
- source=Source.LOGIN,
1108
+ When running in a launch context, the project, entity, and run_id are handled
1109
+ externally, and should be ignored if they are set.
1110
+ """
1111
+ if not self.launch:
1112
+ return
1113
+
1114
+ for key in ("project", "entity", "run_id"):
1115
+ value = getattr(self, key)
1116
+ if value is not None:
1117
+ wandb.termwarn(
1118
+ f"Ignoring {key} {value!r} when running from wandb launch context."
1119
+ )
1120
+ setattr(self, key, None)
1121
+
1122
+ @staticmethod
1123
+ def validate_url(url: str) -> None:
1124
+ """Validate a URL string."""
1125
+ url_validator = SchemaValidator(
1126
+ core_schema.url_schema(
1127
+ allowed_schemes=["http", "https"],
1128
+ strict=True,
1129
+ )
1976
1130
  )
1131
+ url_validator.validate_python(url)
1977
1132
 
1978
- def _apply_run_start(self, run_start_settings: Dict[str, Any]) -> None:
1979
- # This dictionary maps from the "run message dict" to relevant fields in settings
1980
- # Note: that config is missing
1981
- param_map = {
1982
- "run_id": "run_id",
1983
- "entity": "entity",
1984
- "project": "project",
1985
- "run_group": "run_group",
1986
- "job_type": "run_job_type",
1987
- "display_name": "run_name",
1988
- "notes": "run_notes",
1989
- "tags": "run_tags",
1990
- "sweep_id": "sweep_id",
1991
- "host": "host",
1992
- "resumed": "resumed",
1993
- "git.remote_url": "git_remote_url",
1994
- "git.commit": "git_commit",
1995
- }
1996
- run_settings = {
1997
- name: reduce(lambda d, k: d.get(k, {}), attr.split("."), run_start_settings)
1998
- for attr, name in param_map.items()
1999
- }
2000
- run_settings = {key: value for key, value in run_settings.items() if value}
2001
- if run_settings:
2002
- self.update(run_settings, source=Source.RUN)
1133
+ def _get_program(self) -> str | None:
1134
+ """Get the program that started the current process."""
1135
+ if not self._jupyter:
1136
+ # If not in a notebook, try to get the program from the environment
1137
+ # or the __main__ module for scripts run as `python -m ...`.
1138
+ program = os.getenv(env.PROGRAM)
1139
+ if program is not None:
1140
+ return program
1141
+ try:
1142
+ import __main__
1143
+
1144
+ if __main__.__spec__ is None:
1145
+ return __main__.__file__
1146
+ return f"-m {__main__.__spec__.name}"
1147
+ except (ImportError, AttributeError):
1148
+ return None
1149
+ else:
1150
+ # If in a notebook, try to get the program from the notebook metadata.
1151
+ if self.notebook_name:
1152
+ return self.notebook_name
1153
+
1154
+ if not self.x_jupyter_path:
1155
+ return self.program
1156
+
1157
+ if self.x_jupyter_path.startswith("fileId="):
1158
+ return self.x_jupyter_name
1159
+ else:
1160
+ return self.x_jupyter_path
1161
+
1162
+ @staticmethod
1163
+ def _get_program_relpath(program: str, root: str | None = None) -> str | None:
1164
+ """Get the relative path to the program from the root directory."""
1165
+ if not program:
1166
+ return None
1167
+
1168
+ root = root or os.getcwd()
1169
+ if not root:
1170
+ return None
1171
+
1172
+ full_path_to_program = os.path.join(
1173
+ root, os.path.relpath(os.getcwd(), root), program
1174
+ )
1175
+ if os.path.exists(full_path_to_program):
1176
+ relative_path = os.path.relpath(full_path_to_program, start=root)
1177
+ if "../" in relative_path:
1178
+ return None
1179
+ return relative_path
1180
+
1181
+ return None
1182
+
1183
+ @staticmethod
1184
+ def _load_config_file(file_name: str, section: str = "default") -> dict:
1185
+ """Load a config file and return the settings for a given section."""
1186
+ parser = configparser.ConfigParser()
1187
+ parser.add_section(section)
1188
+ parser.read(file_name)
1189
+ config: dict[str, Any] = dict()
1190
+ for k in parser[section]:
1191
+ config[k] = parser[section][k]
1192
+ if k == "ignore_globs":
1193
+ config[k] = config[k].split(",")
1194
+ return config
1195
+
1196
+ def _project_url_base(self) -> str:
1197
+ """Construct the base URL for the project."""
1198
+ if not all([self.entity, self.project]):
1199
+ return ""
1200
+
1201
+ app_url = util.app_url(self.base_url)
1202
+ return f"{app_url}/{quote(self.entity or '')}/{quote(self.project or '')}"
1203
+
1204
+ def _get_url_query_string(self) -> str:
1205
+ """Construct the query string for project, run, and sweep URLs."""
1206
+ # TODO: remove dependency on Api()
1207
+ if Api().settings().get("anonymous") != "true":
1208
+ return ""
1209
+
1210
+ api_key = apikey.api_key(settings=self)
1211
+
1212
+ return f"?{urlencode({'apiKey': api_key})}"
1213
+
1214
+ @staticmethod
1215
+ def _runmoment_preprocessor(val: RunMoment | str | None) -> RunMoment | None:
1216
+ """Preprocess the setting for forking or resuming a run."""
1217
+ if isinstance(val, RunMoment) or val is None:
1218
+ return val
1219
+ elif isinstance(val, str):
1220
+ return RunMoment.from_uri(val)