wandb 0.19.9__py3-none-win32.whl → 0.19.11__py3-none-win32.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 (156) hide show
  1. wandb/__init__.py +1 -1
  2. wandb/__init__.pyi +6 -3
  3. wandb/_pydantic/__init__.py +14 -8
  4. wandb/_pydantic/base.py +51 -36
  5. wandb/_pydantic/utils.py +73 -0
  6. wandb/_pydantic/v1_compat.py +79 -57
  7. wandb/apis/public/__init__.py +2 -2
  8. wandb/apis/public/api.py +684 -4
  9. wandb/apis/public/artifacts.py +377 -677
  10. wandb/apis/public/automations.py +69 -0
  11. wandb/apis/public/integrations.py +180 -0
  12. wandb/apis/public/projects.py +29 -0
  13. wandb/apis/public/registries/__init__.py +0 -0
  14. wandb/apis/public/registries/_freezable_list.py +179 -0
  15. wandb/apis/public/{registries.py → registries/registries_search.py} +22 -129
  16. wandb/apis/public/registries/registry.py +357 -0
  17. wandb/apis/public/registries/utils.py +140 -0
  18. wandb/apis/public/runs.py +58 -56
  19. wandb/apis/public/utils.py +107 -1
  20. wandb/automations/__init__.py +73 -0
  21. wandb/automations/_filters/__init__.py +40 -0
  22. wandb/automations/_filters/expressions.py +181 -0
  23. wandb/automations/_filters/operators.py +258 -0
  24. wandb/automations/_filters/run_metrics.py +332 -0
  25. wandb/automations/_generated/__init__.py +177 -0
  26. wandb/automations/_generated/create_automation.py +17 -0
  27. wandb/automations/_generated/create_generic_webhook_integration.py +43 -0
  28. wandb/automations/_generated/delete_automation.py +17 -0
  29. wandb/automations/_generated/enums.py +33 -0
  30. wandb/automations/_generated/fragments.py +358 -0
  31. wandb/automations/_generated/generic_webhook_integrations_by_entity.py +22 -0
  32. wandb/automations/_generated/get_automations.py +24 -0
  33. wandb/automations/_generated/get_automations_by_entity.py +26 -0
  34. wandb/automations/_generated/input_types.py +104 -0
  35. wandb/automations/_generated/integrations_by_entity.py +22 -0
  36. wandb/automations/_generated/operations.py +647 -0
  37. wandb/automations/_generated/slack_integrations_by_entity.py +22 -0
  38. wandb/automations/_generated/update_automation.py +17 -0
  39. wandb/automations/_utils.py +237 -0
  40. wandb/automations/_validators.py +165 -0
  41. wandb/automations/actions.py +220 -0
  42. wandb/automations/automations.py +87 -0
  43. wandb/automations/events.py +287 -0
  44. wandb/automations/integrations.py +45 -0
  45. wandb/automations/scopes.py +78 -0
  46. wandb/beta/workflows.py +9 -10
  47. wandb/bin/gpu_stats.exe +0 -0
  48. wandb/bin/wandb-core +0 -0
  49. wandb/cli/cli.py +3 -3
  50. wandb/env.py +11 -0
  51. wandb/integration/keras/keras.py +2 -1
  52. wandb/integration/langchain/wandb_tracer.py +2 -1
  53. wandb/jupyter.py +137 -118
  54. wandb/old/settings.py +4 -1
  55. wandb/old/summary.py +0 -2
  56. wandb/proto/v3/wandb_internal_pb2.py +297 -292
  57. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  58. wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
  59. wandb/proto/v4/wandb_internal_pb2.py +292 -292
  60. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  61. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  62. wandb/proto/v5/wandb_internal_pb2.py +292 -292
  63. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  64. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  65. wandb/proto/v6/wandb_base_pb2.py +41 -0
  66. wandb/proto/v6/wandb_internal_pb2.py +393 -0
  67. wandb/proto/v6/wandb_server_pb2.py +78 -0
  68. wandb/proto/v6/wandb_settings_pb2.py +58 -0
  69. wandb/proto/v6/wandb_telemetry_pb2.py +52 -0
  70. wandb/proto/wandb_base_pb2.py +2 -0
  71. wandb/proto/wandb_deprecated.py +8 -0
  72. wandb/proto/wandb_internal_pb2.py +3 -1
  73. wandb/proto/wandb_server_pb2.py +2 -0
  74. wandb/proto/wandb_settings_pb2.py +2 -0
  75. wandb/proto/wandb_telemetry_pb2.py +2 -0
  76. wandb/sdk/artifacts/_generated/__init__.py +289 -0
  77. wandb/sdk/artifacts/_generated/add_aliases.py +21 -0
  78. wandb/sdk/artifacts/_generated/artifact_collection_membership_files.py +43 -0
  79. wandb/sdk/artifacts/_generated/artifact_version_files.py +36 -0
  80. wandb/sdk/artifacts/_generated/create_artifact_collection_tag_assignments.py +36 -0
  81. wandb/sdk/artifacts/_generated/delete_aliases.py +21 -0
  82. wandb/sdk/artifacts/_generated/delete_artifact_collection_tag_assignments.py +25 -0
  83. wandb/sdk/artifacts/_generated/delete_artifact_portfolio.py +35 -0
  84. wandb/sdk/artifacts/_generated/delete_artifact_sequence.py +35 -0
  85. wandb/sdk/artifacts/_generated/enums.py +17 -0
  86. wandb/sdk/artifacts/_generated/fetch_linked_artifacts.py +67 -0
  87. wandb/sdk/artifacts/_generated/fragments.py +221 -0
  88. wandb/sdk/artifacts/_generated/input_types.py +28 -0
  89. wandb/sdk/artifacts/_generated/move_artifact_collection.py +35 -0
  90. wandb/sdk/artifacts/_generated/operations.py +611 -0
  91. wandb/sdk/artifacts/_generated/project_artifact_collection.py +101 -0
  92. wandb/sdk/artifacts/_generated/project_artifact_collections.py +33 -0
  93. wandb/sdk/artifacts/_generated/project_artifact_type.py +24 -0
  94. wandb/sdk/artifacts/_generated/project_artifact_types.py +24 -0
  95. wandb/sdk/artifacts/_generated/project_artifacts.py +42 -0
  96. wandb/sdk/artifacts/_generated/run_input_artifacts.py +51 -0
  97. wandb/sdk/artifacts/_generated/run_output_artifacts.py +51 -0
  98. wandb/sdk/artifacts/_generated/update_artifact.py +26 -0
  99. wandb/sdk/artifacts/_generated/update_artifact_portfolio.py +35 -0
  100. wandb/sdk/artifacts/_generated/update_artifact_sequence.py +35 -0
  101. wandb/sdk/artifacts/_graphql_fragments.py +57 -79
  102. wandb/sdk/artifacts/_validators.py +120 -1
  103. wandb/sdk/artifacts/artifact.py +419 -215
  104. wandb/sdk/artifacts/artifact_file_cache.py +4 -6
  105. wandb/sdk/artifacts/artifact_manifest_entry.py +13 -3
  106. wandb/sdk/artifacts/storage_handlers/azure_handler.py +1 -0
  107. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +182 -1
  108. wandb/sdk/artifacts/storage_policy.py +3 -0
  109. wandb/sdk/data_types/base_types/media.py +2 -3
  110. wandb/sdk/data_types/base_types/wb_value.py +34 -11
  111. wandb/sdk/data_types/html.py +36 -9
  112. wandb/sdk/data_types/image.py +12 -12
  113. wandb/sdk/data_types/table.py +5 -0
  114. wandb/sdk/data_types/trace_tree.py +2 -0
  115. wandb/sdk/data_types/utils.py +1 -1
  116. wandb/sdk/data_types/video.py +59 -57
  117. wandb/sdk/interface/interface.py +4 -3
  118. wandb/sdk/internal/internal_api.py +21 -31
  119. wandb/sdk/internal/profiler.py +6 -5
  120. wandb/sdk/internal/run.py +13 -6
  121. wandb/sdk/internal/sender.py +5 -2
  122. wandb/sdk/launch/sweeps/utils.py +8 -0
  123. wandb/sdk/lib/apikey.py +25 -4
  124. wandb/sdk/lib/asyncio_compat.py +1 -1
  125. wandb/sdk/lib/deprecate.py +13 -22
  126. wandb/sdk/lib/disabled.py +2 -1
  127. wandb/sdk/lib/printer.py +37 -8
  128. wandb/sdk/lib/printer_asyncio.py +46 -0
  129. wandb/sdk/lib/redirect.py +10 -5
  130. wandb/sdk/projects/_generated/__init__.py +47 -0
  131. wandb/sdk/projects/_generated/delete_project.py +22 -0
  132. wandb/sdk/projects/_generated/enums.py +4 -0
  133. wandb/sdk/projects/_generated/fetch_registry.py +22 -0
  134. wandb/sdk/projects/_generated/fragments.py +41 -0
  135. wandb/sdk/projects/_generated/input_types.py +13 -0
  136. wandb/sdk/projects/_generated/operations.py +88 -0
  137. wandb/sdk/projects/_generated/rename_project.py +27 -0
  138. wandb/sdk/projects/_generated/upsert_registry_project.py +27 -0
  139. wandb/sdk/service/server_sock.py +19 -14
  140. wandb/sdk/service/service.py +18 -8
  141. wandb/sdk/service/streams.py +5 -0
  142. wandb/sdk/verify/verify.py +6 -3
  143. wandb/sdk/wandb_init.py +217 -70
  144. wandb/sdk/wandb_login.py +13 -4
  145. wandb/sdk/wandb_run.py +419 -295
  146. wandb/sdk/wandb_settings.py +27 -10
  147. wandb/sdk/wandb_setup.py +61 -0
  148. wandb/util.py +33 -29
  149. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/METADATA +5 -5
  150. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/RECORD +153 -83
  151. wandb/_globals.py +0 -19
  152. wandb/sdk/internal/_generated/base.py +0 -226
  153. wandb/sdk/internal/_generated/typing_compat.py +0 -14
  154. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/WHEEL +0 -0
  155. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/entry_points.txt +0 -0
  156. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/licenses/LICENSE +0 -0
wandb/apis/public/api.py CHANGED
@@ -15,32 +15,62 @@ import json
15
15
  import logging
16
16
  import os
17
17
  import urllib
18
- from typing import Any, Dict, List, Optional
18
+ from http import HTTPStatus
19
+ from typing import (
20
+ TYPE_CHECKING,
21
+ Any,
22
+ Dict,
23
+ Iterator,
24
+ List,
25
+ Literal,
26
+ Optional,
27
+ Set,
28
+ Union,
29
+ )
19
30
 
20
31
  import requests
32
+ from pydantic import ValidationError
33
+ from typing_extensions import Unpack
21
34
  from wandb_gql import Client, gql
22
35
  from wandb_gql.client import RetryError
23
36
 
24
37
  import wandb
25
38
  from wandb import env, util
39
+ from wandb._iterutils import one
26
40
  from wandb.apis import public
27
41
  from wandb.apis.normalize import normalize_exceptions
28
42
  from wandb.apis.public.const import RETRY_TIMEDELTA
29
- from wandb.apis.public.registries import Registries
43
+ from wandb.apis.public.registries.registries_search import Registries
44
+ from wandb.apis.public.registries.registry import Registry
45
+ from wandb.apis.public.registries.utils import _fetch_org_entity_from_organization
30
46
  from wandb.apis.public.utils import (
31
47
  PathType,
32
48
  fetch_org_from_settings_or_entity,
49
+ gql_compat,
33
50
  parse_org_from_registry_path,
34
51
  )
52
+ from wandb.proto.wandb_deprecated import Deprecated
35
53
  from wandb.proto.wandb_internal_pb2 import ServerFeature
36
54
  from wandb.sdk.artifacts._validators import is_artifact_registry_project
37
55
  from wandb.sdk.internal.internal_api import Api as InternalApi
38
56
  from wandb.sdk.internal.thread_local_settings import _thread_local_api_settings
39
57
  from wandb.sdk.launch.utils import LAUNCH_DEFAULT_PROJECT
40
58
  from wandb.sdk.lib import retry, runid
41
- from wandb.sdk.lib.deprecate import Deprecated, deprecate
59
+ from wandb.sdk.lib.deprecate import deprecate
42
60
  from wandb.sdk.lib.gql_request import GraphQLSession
43
61
 
62
+ if TYPE_CHECKING:
63
+ from wandb.automations import (
64
+ ActionType,
65
+ Automation,
66
+ EventType,
67
+ Integration,
68
+ NewAutomation,
69
+ SlackIntegration,
70
+ WebhookIntegration,
71
+ )
72
+ from wandb.automations._utils import WriteAutomationsKwargs
73
+
44
74
  logger = logging.getLogger(__name__)
45
75
 
46
76
 
@@ -1264,7 +1294,11 @@ class Api:
1264
1294
 
1265
1295
  # If its an Registry artifact, the entity is an org instead
1266
1296
  if is_artifact_registry_project(project):
1267
- organization = name.split("/")[0] if name.count("/") == 2 else ""
1297
+ organization = (
1298
+ name.split("/")[0]
1299
+ if name.count("/") == 2
1300
+ else self.settings["organization"]
1301
+ )
1268
1302
  # set entity to match the settings since in above code it was potentially set to an org
1269
1303
  settings_entity = self.settings["entity"] or self.default_entity
1270
1304
  # Registry artifacts are under the org entity. Because we offer a shorthand and alias for this path,
@@ -1517,3 +1551,649 @@ class Api:
1517
1551
  self.settings, self.default_entity
1518
1552
  )
1519
1553
  return Registries(self.client, organization, filter)
1554
+
1555
+ def registry(self, name: str, organization: Optional[str] = None) -> Registry:
1556
+ """Return a registry given a registry name.
1557
+
1558
+ Args:
1559
+ name: The name of the registry. This is without the `wandb-registry-`
1560
+ prefix.
1561
+ organization: The organization of the registry.
1562
+ If no organization is set in the settings, the organization will be
1563
+ fetched from the entity if the entity only belongs to one
1564
+ organization.
1565
+
1566
+ Returns:
1567
+ A registry object.
1568
+
1569
+ Examples:
1570
+ Fetch and update a registry
1571
+ ```python
1572
+ import wandb
1573
+
1574
+ api = wandb.Api()
1575
+ registry = api.registry(name="my-registry", organization="my-org")
1576
+ registry.description = "This is an updated description"
1577
+ registry.save()
1578
+ ```
1579
+ """
1580
+ if not InternalApi()._check_server_feature_with_fallback(
1581
+ ServerFeature.ARTIFACT_REGISTRY_SEARCH
1582
+ ):
1583
+ raise RuntimeError(
1584
+ "api.registry() is not enabled on this wandb server version. "
1585
+ "Please upgrade your server version or contact support at support@wandb.com."
1586
+ )
1587
+ organization = organization or fetch_org_from_settings_or_entity(
1588
+ self.settings, self.default_entity
1589
+ )
1590
+ org_entity = _fetch_org_entity_from_organization(self.client, organization)
1591
+ registry = Registry(self.client, organization, org_entity, name)
1592
+ registry.load()
1593
+ return registry
1594
+
1595
+ def create_registry(
1596
+ self,
1597
+ name: str,
1598
+ visibility: Literal["organization", "restricted"],
1599
+ organization: Optional[str] = None,
1600
+ description: Optional[str] = None,
1601
+ artifact_types: Optional[List[str]] = None,
1602
+ ) -> Registry:
1603
+ """Create a new registry.
1604
+
1605
+ Args:
1606
+ name: The name of the registry. Name must be unique within the organization.
1607
+ visibility: The visibility of the registry.
1608
+ organization: Anyone in the organization can view this registry. You can
1609
+ edit their roles later from the settings in the UI.
1610
+ restricted: Only invited members via the UI can access this registry.
1611
+ Public sharing is disabled.
1612
+ organization: The organization of the registry.
1613
+ If no organization is set in the settings, the organization will be
1614
+ fetched from the entity if the entity only belongs to one organization.
1615
+ description: The description of the registry.
1616
+ artifact_types: The accepted artifact types of the registry. A type is no
1617
+ more than 128 characters and do not include characters `/` or `:`. If
1618
+ not specified, all types are accepted.
1619
+ Allowed types added to the registry cannot be removed later.
1620
+
1621
+ Returns:
1622
+ A registry object.
1623
+
1624
+ Examples:
1625
+ ```python
1626
+ import wandb
1627
+
1628
+ api = wandb.Api()
1629
+ registry = api.create_registry(
1630
+ name="my-registry",
1631
+ visibility="restricted",
1632
+ organization="my-org",
1633
+ description="This is a test registry",
1634
+ artifact_types=["model"],
1635
+ )
1636
+ ```
1637
+ """
1638
+ if not InternalApi()._check_server_feature_with_fallback(
1639
+ ServerFeature.INCLUDE_ARTIFACT_TYPES_IN_REGISTRY_CREATION
1640
+ ):
1641
+ raise RuntimeError(
1642
+ "create_registry api is not enabled on this wandb server version. "
1643
+ "Please upgrade your server version or contact support at support@wandb.com."
1644
+ )
1645
+
1646
+ organization = organization or fetch_org_from_settings_or_entity(
1647
+ self.settings, self.default_entity
1648
+ )
1649
+
1650
+ try:
1651
+ existing_registry = self.registry(name=name, organization=organization)
1652
+ except ValueError:
1653
+ existing_registry = None
1654
+ if existing_registry:
1655
+ raise ValueError(
1656
+ f"Registry {name!r} already exists in organization {organization!r},"
1657
+ " please use a different name."
1658
+ )
1659
+
1660
+ return Registry.create(
1661
+ self.client,
1662
+ organization,
1663
+ name,
1664
+ visibility,
1665
+ description,
1666
+ artifact_types,
1667
+ )
1668
+
1669
+ def integrations(
1670
+ self,
1671
+ entity: Optional[str] = None,
1672
+ *,
1673
+ per_page: int = 50,
1674
+ ) -> Iterator["Integration"]:
1675
+ """Return an iterator of all integrations for an entity.
1676
+
1677
+ Args:
1678
+ entity: The entity (e.g. team name) for which to
1679
+ fetch integrations. If not provided, the user's default entity
1680
+ will be used.
1681
+ per_page: Number of integrations to fetch per page.
1682
+ Defaults to 50. Usually there is no reason to change this.
1683
+
1684
+ Yields:
1685
+ Iterator[SlackIntegration | WebhookIntegration]: An iterator of any supported integrations.
1686
+ """
1687
+ from wandb.apis.public.integrations import Integrations
1688
+
1689
+ params = {"entityName": entity or self.default_entity}
1690
+ return Integrations(client=self.client, variables=params, per_page=per_page)
1691
+
1692
+ def webhook_integrations(
1693
+ self, entity: Optional[str] = None, *, per_page: int = 50
1694
+ ) -> Iterator["WebhookIntegration"]:
1695
+ """Returns an iterator of webhook integrations for an entity.
1696
+
1697
+ Args:
1698
+ entity: The entity (e.g. team name) for which to
1699
+ fetch integrations. If not provided, the user's default entity
1700
+ will be used.
1701
+ per_page: Number of integrations to fetch per page.
1702
+ Defaults to 50. Usually there is no reason to change this.
1703
+
1704
+ Yields:
1705
+ Iterator[WebhookIntegration]: An iterator of webhook integrations.
1706
+
1707
+ Examples:
1708
+ Get all registered webhook integrations for the team "my-team":
1709
+ ```python
1710
+ import wandb
1711
+
1712
+ api = wandb.Api()
1713
+ webhook_integrations = api.webhook_integrations(entity="my-team")
1714
+ ```
1715
+
1716
+ Find only webhook integrations that post requests to "https://my-fake-url.com":
1717
+ ```python
1718
+ webhook_integrations = api.webhook_integrations(entity="my-team")
1719
+ my_webhooks = [
1720
+ ig
1721
+ for ig in webhook_integrations
1722
+ if ig.url_endpoint.startswith("https://my-fake-url.com")
1723
+ ]
1724
+ ```
1725
+ """
1726
+ from wandb.apis.public.integrations import WebhookIntegrations
1727
+
1728
+ params = {"entityName": entity or self.default_entity}
1729
+ return WebhookIntegrations(
1730
+ client=self.client, variables=params, per_page=per_page
1731
+ )
1732
+
1733
+ def slack_integrations(
1734
+ self, *, entity: Optional[str] = None, per_page: int = 50
1735
+ ) -> Iterator["SlackIntegration"]:
1736
+ """Returns an iterator of Slack integrations for an entity.
1737
+
1738
+ Args:
1739
+ entity: The entity (e.g. team name) for which to
1740
+ fetch integrations. If not provided, the user's default entity
1741
+ will be used.
1742
+ per_page: Number of integrations to fetch per page.
1743
+ Defaults to 50. Usually there is no reason to change this.
1744
+
1745
+ Yields:
1746
+ Iterator[SlackIntegration]: An iterator of Slack integrations.
1747
+
1748
+ Examples:
1749
+ Get all registered Slack integrations for the team "my-team":
1750
+ ```python
1751
+ import wandb
1752
+
1753
+ api = wandb.Api()
1754
+ slack_integrations = api.slack_integrations(entity="my-team")
1755
+ ```
1756
+
1757
+ Find only Slack integrations that post to channel names starting with "team-alerts-":
1758
+ ```python
1759
+ slack_integrations = api.slack_integrations(entity="my-team")
1760
+ team_alert_integrations = [
1761
+ ig
1762
+ for ig in slack_integrations
1763
+ if ig.channel_name.startswith("team-alerts-")
1764
+ ]
1765
+ ```
1766
+ """
1767
+ from wandb.apis.public.integrations import SlackIntegrations
1768
+
1769
+ params = {"entityName": entity or self.default_entity}
1770
+ return SlackIntegrations(
1771
+ client=self.client, variables=params, per_page=per_page
1772
+ )
1773
+
1774
+ def _supports_automation(
1775
+ self,
1776
+ *,
1777
+ event: Optional["EventType"] = None,
1778
+ action: Optional["ActionType"] = None,
1779
+ ) -> bool:
1780
+ """Returns whether the server recognizes the automation event and/or action."""
1781
+ from wandb.automations._utils import (
1782
+ ALWAYS_SUPPORTED_ACTIONS,
1783
+ ALWAYS_SUPPORTED_EVENTS,
1784
+ )
1785
+
1786
+ server_features = InternalApi()._server_features()
1787
+ return bool(
1788
+ (
1789
+ (event is None)
1790
+ or (event in ALWAYS_SUPPORTED_EVENTS)
1791
+ or server_features.get(f"AUTOMATION_EVENT_{event.value}")
1792
+ )
1793
+ and (
1794
+ (action is None)
1795
+ or (action in ALWAYS_SUPPORTED_ACTIONS)
1796
+ or server_features.get(f"AUTOMATION_ACTION_{action.value}")
1797
+ )
1798
+ )
1799
+
1800
+ def _omitted_automation_fragments(self) -> Set[str]:
1801
+ """Returns the names of unsupported automation-related fragments.
1802
+
1803
+ Older servers won't recognize newer GraphQL types, so a valid request may
1804
+ unnecessarily error out because it won't recognize fragments defined on those types.
1805
+
1806
+ So e.g. if a server does not support `NO_OP` action types, then the following need to be
1807
+ removed from the body of the GraphQL request:
1808
+
1809
+ - Fragment definition:
1810
+ ```
1811
+ fragment NoOpActionFields on NoOpTriggeredAction {
1812
+ noOp
1813
+ }
1814
+ ```
1815
+
1816
+ - Fragment spread in selection set:
1817
+ ```
1818
+ {
1819
+ ...NoOpActionFields
1820
+ # ... other fields ...
1821
+ }
1822
+ ```
1823
+ """
1824
+ from wandb.automations import ActionType
1825
+ from wandb.automations._generated import (
1826
+ GenericWebhookActionFields,
1827
+ NoOpActionFields,
1828
+ NotificationActionFields,
1829
+ QueueJobActionFields,
1830
+ )
1831
+
1832
+ # Note: we can't currently define this as a constant outside the method
1833
+ # and still keep it nearby in this module, because it relies on pydantic v2-only imports
1834
+ fragment_names: dict[ActionType, str] = {
1835
+ ActionType.NO_OP: NoOpActionFields.__name__,
1836
+ ActionType.QUEUE_JOB: QueueJobActionFields.__name__,
1837
+ ActionType.NOTIFICATION: NotificationActionFields.__name__,
1838
+ ActionType.GENERIC_WEBHOOK: GenericWebhookActionFields.__name__,
1839
+ }
1840
+
1841
+ return set(
1842
+ name
1843
+ for action in ActionType
1844
+ if (not self._supports_automation(action=action))
1845
+ and (name := fragment_names.get(action))
1846
+ )
1847
+
1848
+ def automation(
1849
+ self,
1850
+ name: str,
1851
+ *,
1852
+ entity: Optional[str] = None,
1853
+ ) -> "Automation":
1854
+ """Returns the only Automation matching the parameters.
1855
+
1856
+ Args:
1857
+ name: The name of the automation to fetch.
1858
+ entity: The entity to fetch the automation for.
1859
+
1860
+ Raises:
1861
+ ValueError: If zero or multiple Automations match the search criteria.
1862
+
1863
+ Examples:
1864
+ Get an existing automation named "my-automation":
1865
+
1866
+ ```python
1867
+ import wandb
1868
+
1869
+ api = wandb.Api()
1870
+ automation = api.automation(name="my-automation")
1871
+ ```
1872
+
1873
+ Get an existing automation named "other-automation", from the entity "my-team":
1874
+
1875
+ ```python
1876
+ automation = api.automation(name="other-automation", entity="my-team")
1877
+ ```
1878
+ """
1879
+ return one(
1880
+ self.automations(entity=entity, name=name),
1881
+ too_short=ValueError("No automations found"),
1882
+ too_long=ValueError("Multiple automations found"),
1883
+ )
1884
+
1885
+ def automations(
1886
+ self,
1887
+ entity: Optional[str] = None,
1888
+ *,
1889
+ name: Optional[str] = None,
1890
+ per_page: int = 50,
1891
+ ) -> Iterator["Automation"]:
1892
+ """Returns an iterator over all Automations that match the given parameters.
1893
+
1894
+ If no parameters are provided, the returned iterator will contain all
1895
+ Automations that the user has access to.
1896
+
1897
+ Args:
1898
+ entity: The entity to fetch the automations for.
1899
+ name: The name of the automation to fetch.
1900
+ per_page: The number of automations to fetch per page.
1901
+ Defaults to 50. Usually there is no reason to change this.
1902
+
1903
+ Returns:
1904
+ A list of automations.
1905
+
1906
+ Examples:
1907
+ Fetch all existing automations for the entity "my-team":
1908
+
1909
+ ```python
1910
+ import wandb
1911
+
1912
+ api = wandb.Api()
1913
+ automations = api.automations(entity="my-team")
1914
+ ```
1915
+ """
1916
+ from wandb.apis.public.automations import Automations
1917
+ from wandb.automations._generated import (
1918
+ GET_AUTOMATIONS_BY_ENTITY_GQL,
1919
+ GET_AUTOMATIONS_GQL,
1920
+ )
1921
+
1922
+ # For now, we need to use different queries depending on whether entity is given
1923
+ variables = {"entityName": entity}
1924
+ if entity is None:
1925
+ gql_str = GET_AUTOMATIONS_GQL # Automations for viewer
1926
+ else:
1927
+ gql_str = GET_AUTOMATIONS_BY_ENTITY_GQL # Automations for entity
1928
+
1929
+ # If needed, rewrite the GraphQL field selection set to omit unsupported fields/fragments/types
1930
+ omit_fragments = self._omitted_automation_fragments()
1931
+ query = gql_compat(gql_str, omit_fragments=omit_fragments)
1932
+ iterator = Automations(
1933
+ client=self.client, variables=variables, per_page=per_page, _query=query
1934
+ )
1935
+
1936
+ # FIXME: this is crude, move this client-side filtering logic into backend
1937
+ if name is not None:
1938
+ iterator = filter(lambda x: x.name == name, iterator)
1939
+ yield from iterator
1940
+
1941
+ def create_automation(
1942
+ self,
1943
+ obj: "NewAutomation",
1944
+ *,
1945
+ fetch_existing: bool = False,
1946
+ **kwargs: Unpack["WriteAutomationsKwargs"],
1947
+ ) -> "Automation":
1948
+ """Create a new Automation.
1949
+
1950
+ Args:
1951
+ obj:
1952
+ The automation to create.
1953
+ fetch_existing:
1954
+ If True, and a conflicting automation already exists, attempt
1955
+ to fetch the existing automation instead of raising an error.
1956
+ **kwargs:
1957
+ Any additional values to assign to the automation before
1958
+ creating it. If given, these will override any values that may
1959
+ already be set on the automation:
1960
+ - `name`: The name of the automation.
1961
+ - `description`: The description of the automation.
1962
+ - `enabled`: Whether the automation is enabled.
1963
+ - `scope`: The scope of the automation.
1964
+ - `event`: The event that triggers the automation.
1965
+ - `action`: The action that is triggered by the automation.
1966
+
1967
+ Returns:
1968
+ The saved Automation.
1969
+
1970
+ Examples:
1971
+ Create a new automation named "my-automation" that sends a Slack notification
1972
+ when a run within a specific project logs a metric exceeding a custom threshold:
1973
+
1974
+ ```python
1975
+ import wandb
1976
+ from wandb.automations import OnRunMetric, RunEvent, SendNotification
1977
+
1978
+ api = wandb.Api()
1979
+
1980
+ project = api.project("my-project", entity="my-team")
1981
+
1982
+ # Use the first Slack integration for the team
1983
+ slack_hook = next(api.slack_integrations(entity="my-team"))
1984
+
1985
+ event = OnRunMetric(
1986
+ scope=project,
1987
+ filter=RunEvent.metric("custom-metric") > 10,
1988
+ )
1989
+ action = SendNotification.from_integration(slack_hook)
1990
+
1991
+ automation = api.create_automation(
1992
+ event >> action,
1993
+ name="my-automation",
1994
+ description="Send a Slack message whenever 'custom-metric' exceeds 10.",
1995
+ )
1996
+ ```
1997
+ """
1998
+ from wandb.automations import Automation
1999
+ from wandb.automations._generated import CREATE_AUTOMATION_GQL, CreateAutomation
2000
+ from wandb.automations._utils import prepare_to_create
2001
+
2002
+ gql_input = prepare_to_create(obj, **kwargs)
2003
+
2004
+ if not self._supports_automation(
2005
+ event=(event := gql_input.triggering_event_type),
2006
+ action=(action := gql_input.triggered_action_type),
2007
+ ):
2008
+ raise ValueError(
2009
+ f"Automation event or action ({event!r} -> {action!r}) "
2010
+ "is not supported on this wandb server version. "
2011
+ "Please upgrade your server version, or contact support at "
2012
+ "support@wandb.com."
2013
+ )
2014
+
2015
+ # If needed, rewrite the GraphQL field selection set to omit unsupported fields/fragments/types
2016
+ omit_fragments = self._omitted_automation_fragments()
2017
+ mutation = gql_compat(CREATE_AUTOMATION_GQL, omit_fragments=omit_fragments)
2018
+ variables = {"params": gql_input.model_dump(exclude_none=True)}
2019
+
2020
+ name = gql_input.name
2021
+ try:
2022
+ data = self.client.execute(mutation, variable_values=variables)
2023
+ except requests.HTTPError as e:
2024
+ status = HTTPStatus(e.response.status_code)
2025
+ if status is HTTPStatus.CONFLICT: # 409
2026
+ if fetch_existing:
2027
+ wandb.termlog(f"Automation {name!r} exists. Fetching it instead.")
2028
+ return self.automation(name=name)
2029
+
2030
+ raise ValueError(
2031
+ f"Automation {name!r} exists. Unable to create another with the same name."
2032
+ ) from None
2033
+ raise
2034
+
2035
+ try:
2036
+ result = CreateAutomation.model_validate(data).result
2037
+ except ValidationError as e:
2038
+ msg = f"Invalid response while creating automation {name!r}"
2039
+ raise RuntimeError(msg) from e
2040
+
2041
+ if (result is None) or (result.trigger is None):
2042
+ msg = f"Empty response while creating automation {name!r}"
2043
+ raise RuntimeError(msg)
2044
+
2045
+ return Automation.model_validate(result.trigger)
2046
+
2047
+ def update_automation(
2048
+ self,
2049
+ obj: "Automation",
2050
+ *,
2051
+ create_missing: bool = False,
2052
+ **kwargs: Unpack["WriteAutomationsKwargs"],
2053
+ ) -> "Automation":
2054
+ """Update an existing automation.
2055
+
2056
+ Args:
2057
+ obj: The automation to update. Must be an existing automation.
2058
+ create_missing (bool):
2059
+ If True, and the automation does not exist, create it.
2060
+ **kwargs:
2061
+ Any additional values to assign to the automation before
2062
+ updating it. If given, these will override any values that may
2063
+ already be set on the automation:
2064
+ - `name`: The name of the automation.
2065
+ - `description`: The description of the automation.
2066
+ - `enabled`: Whether the automation is enabled.
2067
+ - `scope`: The scope of the automation.
2068
+ - `event`: The event that triggers the automation.
2069
+ - `action`: The action that is triggered by the automation.
2070
+
2071
+ Returns:
2072
+ The updated automation.
2073
+
2074
+ Examples:
2075
+ Disable and edit the description of an existing automation ("my-automation"):
2076
+
2077
+ ```python
2078
+ import wandb
2079
+
2080
+ api = wandb.Api()
2081
+
2082
+ automation = api.automation(name="my-automation")
2083
+ automation.enabled = False
2084
+ automation.description = "Kept for reference, but no longer used."
2085
+
2086
+ updated_automation = api.update_automation(automation)
2087
+ ```
2088
+
2089
+ OR:
2090
+
2091
+ ```python
2092
+ import wandb
2093
+
2094
+ api = wandb.Api()
2095
+
2096
+ automation = api.automation(name="my-automation")
2097
+
2098
+ updated_automation = api.update_automation(
2099
+ automation,
2100
+ enabled=False,
2101
+ description="Kept for reference, but no longer used.",
2102
+ )
2103
+ ```
2104
+ """
2105
+ from wandb.automations import ActionType, Automation
2106
+ from wandb.automations._generated import UPDATE_AUTOMATION_GQL, UpdateAutomation
2107
+ from wandb.automations._utils import prepare_to_update
2108
+
2109
+ # Check if the server even supports updating automations.
2110
+ #
2111
+ # NOTE: Unfortunately, there is no current server feature flag for this. As a workaround,
2112
+ # we check whether the server supports the NO_OP action, which is a reasonably safe proxy
2113
+ # for whether it supports updating automations.
2114
+ if not self._supports_automation(action=ActionType.NO_OP):
2115
+ raise RuntimeError(
2116
+ "Updating existing automations is not enabled on this wandb server version. "
2117
+ "Please upgrade your server version, or contact support at support@wandb.com."
2118
+ )
2119
+
2120
+ gql_input = prepare_to_update(obj, **kwargs)
2121
+
2122
+ if not self._supports_automation(
2123
+ event=(event := gql_input.triggering_event_type),
2124
+ action=(action := gql_input.triggered_action_type),
2125
+ ):
2126
+ raise ValueError(
2127
+ f"Automation event or action ({event.value} -> {action.value}) "
2128
+ "is not supported on this wandb server version. "
2129
+ "Please upgrade your server version, or contact support at "
2130
+ "support@wandb.com."
2131
+ )
2132
+
2133
+ # If needed, rewrite the GraphQL field selection set to omit unsupported fields/fragments/types
2134
+ omit_fragments = self._omitted_automation_fragments()
2135
+ mutation = gql_compat(UPDATE_AUTOMATION_GQL, omit_fragments=omit_fragments)
2136
+ variables = {"params": gql_input.model_dump(exclude_none=True)}
2137
+
2138
+ name = gql_input.name
2139
+ try:
2140
+ data = self.client.execute(mutation, variable_values=variables)
2141
+ except requests.HTTPError as e:
2142
+ status = HTTPStatus(e.response.status_code)
2143
+ if status is HTTPStatus.NOT_FOUND: # 404
2144
+ if create_missing:
2145
+ wandb.termlog(f"Automation {name!r} not found. Creating it.")
2146
+ return self.create_automation(obj)
2147
+
2148
+ raise ValueError(
2149
+ f"Automation {name!r} not found. Unable to edit it."
2150
+ ) from e
2151
+
2152
+ # Not a (known) recoverable HTTP error
2153
+ wandb.termerror(f"Got response status {status!r}: {e.response.text!r}")
2154
+ raise e
2155
+
2156
+ try:
2157
+ result = UpdateAutomation.model_validate(data).result
2158
+ except ValidationError as e:
2159
+ msg = f"Invalid response while updating automation {name!r}"
2160
+ raise RuntimeError(msg) from e
2161
+
2162
+ if (result is None) or (result.trigger is None):
2163
+ msg = f"Empty response while updating automation {name!r}"
2164
+ raise RuntimeError(msg)
2165
+
2166
+ return Automation.model_validate(result.trigger)
2167
+
2168
+ def delete_automation(self, obj: Union["Automation", str]) -> Literal[True]:
2169
+ """Delete an automation.
2170
+
2171
+ Args:
2172
+ obj: The automation to delete, or its ID.
2173
+
2174
+ Returns:
2175
+ True if the automation was deleted successfully.
2176
+ """
2177
+ from wandb.automations._generated import DELETE_AUTOMATION_GQL, DeleteAutomation
2178
+ from wandb.automations._utils import extract_id
2179
+
2180
+ id_ = extract_id(obj)
2181
+ mutation = gql(DELETE_AUTOMATION_GQL)
2182
+ variables = {"id": id_}
2183
+
2184
+ data = self.client.execute(mutation, variable_values=variables)
2185
+
2186
+ try:
2187
+ result = DeleteAutomation.model_validate(data).result
2188
+ except ValidationError as e:
2189
+ msg = f"Invalid response while deleting automation {id_!r}"
2190
+ raise RuntimeError(msg) from e
2191
+
2192
+ if result is None:
2193
+ msg = f"Empty response while deleting automation {id_!r}"
2194
+ raise RuntimeError(msg)
2195
+
2196
+ if not result.success:
2197
+ raise RuntimeError(f"Failed to delete automation: {id_!r}")
2198
+
2199
+ return result.success