wbnetrc.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. from __future__ import annotations
  2. import netrc
  3. import os
  4. import pathlib
  5. import platform
  6. import shlex
  7. from urllib.parse import urlsplit
  8. from wandb.errors import term
  9. from .auth import AuthApiKey, AuthWithSource
  10. from .host_url import HostUrl
  11. class WriteNetrcError(Exception):
  12. """Could not write to the netrc file."""
  13. def read_netrc_auth(*, host: str | HostUrl) -> str | None:
  14. """Read a W&B API key from the .netrc file.
  15. Args:
  16. host: The W&B server URL.
  17. Returns:
  18. An API key for the host, or None if there's no .netrc file
  19. or if it doesn't contain credentials for the specified host.
  20. Raises:
  21. AuthenticationError: If an API key is found but is not in
  22. a valid format.
  23. """
  24. if not isinstance(host, HostUrl):
  25. host = HostUrl(host)
  26. if not (auth := read_netrc_auth_with_source(host=host)):
  27. return None
  28. assert isinstance(auth.auth, AuthApiKey)
  29. return auth.auth.api_key
  30. def read_netrc_auth_with_source(*, host: HostUrl) -> AuthWithSource | None:
  31. """Read a W&B API key from the .netrc file.
  32. Args:
  33. host: The W&B server URL.
  34. Returns:
  35. An API key for the host, or None if there's no .netrc file
  36. or it doesn't contain credentials for the specified host.
  37. Also returns the file in which the API key was found.
  38. Raises:
  39. AuthenticationError: If an API key is found but is not in
  40. a valid format.
  41. """
  42. path = _get_netrc_file_path()
  43. try:
  44. netrc_file = netrc.netrc(path)
  45. except FileNotFoundError:
  46. return None
  47. except (netrc.NetrcParseError, OSError) as e:
  48. if isinstance(e, netrc.NetrcParseError) and e.lineno is not None:
  49. term.termwarn(
  50. f"Failed to read netrc file at {path},"
  51. + f" error on line {e.lineno}: {e.msg}"
  52. )
  53. else:
  54. term.termwarn(f"Failed to read netrc file at {path}: {e}")
  55. return None
  56. if not (netloc := urlsplit(host.url).netloc):
  57. return None
  58. if not (creds := netrc_file.authenticators(netloc)):
  59. return None
  60. _, _, password = creds
  61. if not password:
  62. term.termwarn(f"Found entry for machine {netloc!r} with no API key at {path}")
  63. return None
  64. return AuthWithSource(
  65. auth=AuthApiKey(host=host, api_key=password),
  66. source=str(path),
  67. )
  68. def write_netrc_auth(*, host: str, api_key: str) -> None:
  69. """Store an API key in the .netrc file.
  70. Args:
  71. host: The W&B server URL.
  72. api_key: A valid API key to write.
  73. Raises:
  74. WriteNetrcError: If there's a problem writing to the .netrc file.
  75. """
  76. if not (netloc := urlsplit(host).netloc):
  77. raise ValueError(f"Invalid host URL: {host!r}")
  78. _update_netrc(
  79. _get_netrc_file_path(),
  80. machine=netloc,
  81. password=api_key,
  82. )
  83. def _update_netrc(
  84. path: pathlib.Path,
  85. *,
  86. machine: str,
  87. password: str,
  88. ) -> None:
  89. # Avoid accidentally breaking the user's .netrc file
  90. # given invalid or malicious input.
  91. #
  92. # The .netrc file format allows using quotes in the same way
  93. # as in sh syntax; the built-in netrc library also uses shlex.
  94. machine = shlex.quote(machine)
  95. password = shlex.quote(password)
  96. machine_line = f"machine {machine}"
  97. orig_lines = []
  98. try:
  99. orig_lines = path.read_text().splitlines()
  100. except FileNotFoundError:
  101. term.termlog("No netrc file found, creating one.")
  102. path.touch(mode=0o600) # user readable and writable
  103. except OSError as e:
  104. # Include the original error message because the stack trace
  105. # will not be shown to the user.
  106. raise WriteNetrcError(f"Unable to read {path}: {e}") from e
  107. new_lines: list[str] = []
  108. # Copy over the original lines, minus the machine section we're updating.
  109. skip = 0
  110. for line in orig_lines:
  111. if machine_line in line:
  112. skip = 2
  113. elif skip > 0:
  114. skip -= 1
  115. else:
  116. new_lines.append(line)
  117. new_lines.extend(
  118. [
  119. f"machine {machine}",
  120. " login user",
  121. f" password {password}",
  122. "", # End with a blank line, by convention.
  123. ]
  124. )
  125. term.termlog(f"Appending key for {machine} to your netrc file: {path}")
  126. try:
  127. _write_text(path, "\n".join(new_lines))
  128. except OSError as e:
  129. # Include the original error message because the stack trace
  130. # will not be shown to the user.
  131. raise WriteNetrcError(f"Unable to write {path}: {e}") from e
  132. def _write_text(path: pathlib.Path, text: str) -> None:
  133. """Call pathlib.Path.write_text().
  134. Patched in tests.
  135. """
  136. path.write_text(text)
  137. def _get_netrc_file_path() -> pathlib.Path:
  138. """Returns the path to the .netrc file.
  139. The file at the path may or may not exist.
  140. """
  141. # The environment variable takes priority.
  142. if netrc_file := os.environ.get("NETRC"):
  143. return pathlib.Path(netrc_file).expanduser()
  144. # If a netrc file exists in a standard location, use it.
  145. unix_netrc = pathlib.Path("~/.netrc").expanduser()
  146. if unix_netrc.exists():
  147. return unix_netrc
  148. windows_netrc = pathlib.Path("~/_netrc").expanduser()
  149. if windows_netrc.exists():
  150. return windows_netrc
  151. # Otherwise, use the conventional file based on the platform.
  152. if platform.system() != "Windows":
  153. return unix_netrc
  154. else:
  155. return windows_netrc