users.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. """W&B Public API for managing users and API keys.
  2. This module provides classes for managing W&B users and their API keys.
  3. Note:
  4. This module is part of the W&B Public API and provides methods to manage
  5. users and their authentication. Some operations require admin privileges.
  6. """
  7. from __future__ import annotations
  8. from collections.abc import MutableMapping
  9. from typing import TYPE_CHECKING, Any
  10. from typing_extensions import Self
  11. from wandb_gql import gql
  12. import wandb
  13. from wandb.apis.attrs import Attrs
  14. if TYPE_CHECKING:
  15. from .api import Api, RetryingClient
  16. class User(Attrs):
  17. """A user on a W&B instance.
  18. This allows managing a user's API keys and accessing information like
  19. team memberships. The `create` class method can be used to create a new
  20. user.
  21. Args:
  22. client: The GraphQL client to use for network operations.
  23. attrs: A subset of the User type in the GraphQL schema.
  24. <!-- lazydoc-ignore-init: internal -->
  25. """
  26. def __init__(
  27. self,
  28. client: RetryingClient,
  29. attrs: MutableMapping[str, Any],
  30. api_key: str | None = None,
  31. ):
  32. super().__init__(attrs)
  33. self._client = client
  34. self._api_key = api_key
  35. self._user_api: Api | None = None
  36. @property
  37. def user_api(self) -> Api | None:
  38. """A `wandb.Api` instance using the user's credentials."""
  39. if self._user_api is None and self._api_key:
  40. self._user_api = wandb.Api(api_key=self._api_key)
  41. return self._user_api
  42. @classmethod
  43. def create(cls, api: Api, email: str, admin: bool = False) -> Self:
  44. """Create a new user.
  45. This is an internal method. Use the `create_user()` method of
  46. `wandb.Api` instead.
  47. Args:
  48. api: The API instance to use to create the user.
  49. email: The email for the user.
  50. admin: Whether this user should be a global instance admin.
  51. Returns:
  52. A `User` object.
  53. <!-- lazydoc-ignore-classmethod: internal -->
  54. """
  55. from wandb.apis._generated import (
  56. CREATE_USER_FROM_ADMIN_GQL,
  57. CreateUserFromAdmin,
  58. )
  59. gql_op = gql(CREATE_USER_FROM_ADMIN_GQL)
  60. data = api.client.execute(gql_op, {"email": email, "admin": admin})
  61. user = CreateUserFromAdmin.model_validate(data).result.user
  62. return cls(api.client, user.model_dump(), api_key=api.api_key)
  63. @property
  64. def api_keys(self) -> list[str]:
  65. """Names of the user's API keys.
  66. This property returns the names of the the API keys, *not* the secret
  67. associated with the key. The name of the key cannot be used as an API
  68. key.
  69. The list is empty if the user has no API keys or if API keys have not
  70. been loaded.
  71. """
  72. if self._attrs.get("apiKeys") is None:
  73. return []
  74. return [k["node"]["name"] for k in self._attrs["apiKeys"]["edges"]]
  75. @property
  76. def teams(self) -> list[str]:
  77. """Names of the user's teams.
  78. This is an empty list if the user has no team memberships or if teams
  79. data was not loaded.
  80. """
  81. if self._attrs.get("teams") is None:
  82. return []
  83. return [k["node"]["name"] for k in self._attrs["teams"]["edges"]]
  84. def delete_api_key(self, api_key: str) -> bool:
  85. """Delete a user's API key.
  86. Only the owner of the key or an admin can delete it.
  87. Args:
  88. api_key: The name of the API key to delete. Use one of
  89. the names returned by the `api_keys` property.
  90. Returns:
  91. True on success, false on failure.
  92. """
  93. from requests import HTTPError
  94. from wandb.apis._generated import DELETE_API_KEY_GQL
  95. idx = self.api_keys.index(api_key)
  96. api_key_id = self._attrs["apiKeys"]["edges"][idx]["node"]["id"]
  97. try:
  98. self._client.execute(gql(DELETE_API_KEY_GQL), {"id": api_key_id})
  99. except HTTPError:
  100. return False
  101. return True
  102. def generate_api_key(self, description: str | None = None) -> str | None:
  103. """Generate a new API key.
  104. Args:
  105. description: A description for the new API key. This can be
  106. used to identify the purpose of the API key.
  107. Returns:
  108. The generated API key (the full secret, not just the name), or
  109. None on failure.
  110. """
  111. from requests import HTTPError
  112. from wandb.apis._generated import GENERATE_API_KEY_GQL, GenerateApiKey
  113. try:
  114. # We must make this call using credentials from the original user
  115. gql_op = gql(GENERATE_API_KEY_GQL)
  116. data = self._client.execute(gql_op, {"description": description})
  117. key_fragment = GenerateApiKey.model_validate(data).result.api_key
  118. self._attrs["apiKeys"]["edges"].append({"node": key_fragment.model_dump()})
  119. except (HTTPError, AttributeError):
  120. return None
  121. else:
  122. return key_fragment.name
  123. def __repr__(self) -> str:
  124. if email := self._attrs.get("email"):
  125. return f"<User {email}>"
  126. if username := self._attrs.get("username"):
  127. return f"<User {username}>"
  128. if id_ := self._attrs.get("id"):
  129. return f"<User {id_}>"
  130. if name := self._attrs.get("name"):
  131. return f"<User {name!r}>"
  132. return "<User ???>"