wandb 0.19.10__py3-none-musllinux_1_2_aarch64.whl → 0.19.11__py3-none-musllinux_1_2_aarch64.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 (90) hide show
  1. wandb/__init__.py +1 -1
  2. wandb/__init__.pyi +3 -3
  3. wandb/_pydantic/__init__.py +2 -3
  4. wandb/_pydantic/base.py +11 -31
  5. wandb/_pydantic/utils.py +8 -1
  6. wandb/_pydantic/v1_compat.py +3 -3
  7. wandb/apis/public/api.py +590 -22
  8. wandb/apis/public/artifacts.py +13 -5
  9. wandb/apis/public/automations.py +1 -1
  10. wandb/apis/public/integrations.py +22 -10
  11. wandb/apis/public/registries/__init__.py +0 -0
  12. wandb/apis/public/registries/_freezable_list.py +179 -0
  13. wandb/apis/public/{registries.py → registries/registries_search.py} +22 -129
  14. wandb/apis/public/registries/registry.py +357 -0
  15. wandb/apis/public/registries/utils.py +140 -0
  16. wandb/apis/public/runs.py +58 -56
  17. wandb/automations/__init__.py +16 -24
  18. wandb/automations/_filters/expressions.py +12 -10
  19. wandb/automations/_filters/operators.py +10 -19
  20. wandb/automations/_filters/run_metrics.py +231 -82
  21. wandb/automations/_generated/__init__.py +27 -34
  22. wandb/automations/_generated/create_automation.py +17 -0
  23. wandb/automations/_generated/delete_automation.py +17 -0
  24. wandb/automations/_generated/fragments.py +40 -25
  25. wandb/automations/_generated/{get_triggers.py → get_automations.py} +5 -5
  26. wandb/automations/_generated/{get_triggers_by_entity.py → get_automations_by_entity.py} +7 -5
  27. wandb/automations/_generated/operations.py +35 -98
  28. wandb/automations/_generated/update_automation.py +17 -0
  29. wandb/automations/_utils.py +178 -64
  30. wandb/automations/_validators.py +94 -2
  31. wandb/automations/actions.py +113 -98
  32. wandb/automations/automations.py +47 -69
  33. wandb/automations/events.py +139 -87
  34. wandb/automations/integrations.py +23 -4
  35. wandb/automations/scopes.py +22 -20
  36. wandb/bin/gpu_stats +0 -0
  37. wandb/bin/wandb-core +0 -0
  38. wandb/env.py +11 -0
  39. wandb/old/settings.py +4 -1
  40. wandb/proto/v3/wandb_internal_pb2.py +240 -236
  41. wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
  42. wandb/proto/v4/wandb_internal_pb2.py +236 -236
  43. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  44. wandb/proto/v5/wandb_internal_pb2.py +236 -236
  45. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  46. wandb/proto/v6/wandb_internal_pb2.py +236 -236
  47. wandb/proto/v6/wandb_telemetry_pb2.py +10 -10
  48. wandb/sdk/artifacts/_generated/__init__.py +42 -1
  49. wandb/sdk/artifacts/_generated/add_aliases.py +21 -0
  50. wandb/sdk/artifacts/_generated/delete_aliases.py +21 -0
  51. wandb/sdk/artifacts/_generated/fetch_linked_artifacts.py +67 -0
  52. wandb/sdk/artifacts/_generated/fragments.py +35 -0
  53. wandb/sdk/artifacts/_generated/input_types.py +12 -0
  54. wandb/sdk/artifacts/_generated/operations.py +101 -0
  55. wandb/sdk/artifacts/_generated/update_artifact.py +26 -0
  56. wandb/sdk/artifacts/_graphql_fragments.py +1 -0
  57. wandb/sdk/artifacts/_validators.py +120 -1
  58. wandb/sdk/artifacts/artifact.py +380 -203
  59. wandb/sdk/artifacts/artifact_file_cache.py +4 -6
  60. wandb/sdk/artifacts/artifact_manifest_entry.py +11 -2
  61. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +182 -1
  62. wandb/sdk/artifacts/storage_policy.py +3 -0
  63. wandb/sdk/data_types/video.py +46 -32
  64. wandb/sdk/interface/interface.py +2 -3
  65. wandb/sdk/internal/internal_api.py +21 -31
  66. wandb/sdk/internal/sender.py +5 -2
  67. wandb/sdk/launch/sweeps/utils.py +8 -0
  68. wandb/sdk/projects/_generated/__init__.py +47 -0
  69. wandb/sdk/projects/_generated/delete_project.py +22 -0
  70. wandb/sdk/projects/_generated/enums.py +4 -0
  71. wandb/sdk/projects/_generated/fetch_registry.py +22 -0
  72. wandb/sdk/projects/_generated/fragments.py +41 -0
  73. wandb/sdk/projects/_generated/input_types.py +13 -0
  74. wandb/sdk/projects/_generated/operations.py +88 -0
  75. wandb/sdk/projects/_generated/rename_project.py +27 -0
  76. wandb/sdk/projects/_generated/upsert_registry_project.py +27 -0
  77. wandb/sdk/service/service.py +9 -1
  78. wandb/sdk/wandb_init.py +32 -5
  79. wandb/sdk/wandb_run.py +37 -9
  80. wandb/sdk/wandb_settings.py +6 -7
  81. wandb/sdk/wandb_setup.py +12 -0
  82. wandb/util.py +7 -3
  83. {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/METADATA +1 -1
  84. {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/RECORD +87 -70
  85. wandb/automations/_generated/create_filter_trigger.py +0 -21
  86. wandb/automations/_generated/delete_trigger.py +0 -19
  87. wandb/automations/_generated/update_filter_trigger.py +0 -21
  88. {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/WHEEL +0 -0
  89. {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/entry_points.txt +0 -0
  90. {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/licenses/LICENSE +0 -0
wandb/apis/public/api.py CHANGED
@@ -15,21 +15,38 @@ import json
15
15
  import logging
16
16
  import os
17
17
  import urllib
18
- from typing import TYPE_CHECKING, Any, Dict, Iterator, 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
  )
35
52
  from wandb.proto.wandb_deprecated import Deprecated
@@ -43,7 +60,16 @@ from wandb.sdk.lib.deprecate import deprecate
43
60
  from wandb.sdk.lib.gql_request import GraphQLSession
44
61
 
45
62
  if TYPE_CHECKING:
46
- from wandb.automations import Integration, SlackIntegration, WebhookIntegration
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
47
73
 
48
74
  logger = logging.getLogger(__name__)
49
75
 
@@ -1268,7 +1294,11 @@ class Api:
1268
1294
 
1269
1295
  # If its an Registry artifact, the entity is an org instead
1270
1296
  if is_artifact_registry_project(project):
1271
- 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
+ )
1272
1302
  # set entity to match the settings since in above code it was potentially set to an org
1273
1303
  settings_entity = self.settings["entity"] or self.default_entity
1274
1304
  # Registry artifacts are under the org entity. Because we offer a shorthand and alias for this path,
@@ -1522,6 +1552,120 @@ class Api:
1522
1552
  )
1523
1553
  return Registries(self.client, organization, filter)
1524
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
+
1525
1669
  def integrations(
1526
1670
  self,
1527
1671
  entity: Optional[str] = None,
@@ -1531,32 +1675,31 @@ class Api:
1531
1675
  """Return an iterator of all integrations for an entity.
1532
1676
 
1533
1677
  Args:
1534
- entity (str, optional): The entity (e.g. team name) for which to
1678
+ entity: The entity (e.g. team name) for which to
1535
1679
  fetch integrations. If not provided, the user's default entity
1536
1680
  will be used.
1537
- per_page (int, optional): Number of integrations to fetch per page.
1538
- Defaults to 50.
1681
+ per_page: Number of integrations to fetch per page.
1682
+ Defaults to 50. Usually there is no reason to change this.
1539
1683
 
1540
1684
  Yields:
1541
1685
  Iterator[SlackIntegration | WebhookIntegration]: An iterator of any supported integrations.
1542
1686
  """
1543
1687
  from wandb.apis.public.integrations import Integrations
1544
1688
 
1545
- entity = entity or self.default_entity
1546
- params = {"entityName": entity, "includeWebhook": True, "includeSlack": True}
1689
+ params = {"entityName": entity or self.default_entity}
1547
1690
  return Integrations(client=self.client, variables=params, per_page=per_page)
1548
1691
 
1549
1692
  def webhook_integrations(
1550
1693
  self, entity: Optional[str] = None, *, per_page: int = 50
1551
1694
  ) -> Iterator["WebhookIntegration"]:
1552
- """Return an iterator of webhook integrations for an entity.
1695
+ """Returns an iterator of webhook integrations for an entity.
1553
1696
 
1554
1697
  Args:
1555
- entity (str, optional): The entity (e.g. team name) for which to
1698
+ entity: The entity (e.g. team name) for which to
1556
1699
  fetch integrations. If not provided, the user's default entity
1557
1700
  will be used.
1558
- per_page (int, optional): Number of integrations to fetch per page.
1559
- Defaults to 50.
1701
+ per_page: Number of integrations to fetch per page.
1702
+ Defaults to 50. Usually there is no reason to change this.
1560
1703
 
1561
1704
  Yields:
1562
1705
  Iterator[WebhookIntegration]: An iterator of webhook integrations.
@@ -1582,23 +1725,22 @@ class Api:
1582
1725
  """
1583
1726
  from wandb.apis.public.integrations import WebhookIntegrations
1584
1727
 
1585
- entity = entity or self.default_entity
1586
- params = {"entityName": entity, "includeWebhook": True}
1728
+ params = {"entityName": entity or self.default_entity}
1587
1729
  return WebhookIntegrations(
1588
1730
  client=self.client, variables=params, per_page=per_page
1589
1731
  )
1590
1732
 
1591
1733
  def slack_integrations(
1592
- self, entity: Optional[str] = None, *, per_page: int = 50
1734
+ self, *, entity: Optional[str] = None, per_page: int = 50
1593
1735
  ) -> Iterator["SlackIntegration"]:
1594
- """Return an iterator of Slack integrations for an entity.
1736
+ """Returns an iterator of Slack integrations for an entity.
1595
1737
 
1596
1738
  Args:
1597
- entity (str, optional): The entity (e.g. team name) for which to
1739
+ entity: The entity (e.g. team name) for which to
1598
1740
  fetch integrations. If not provided, the user's default entity
1599
1741
  will be used.
1600
- per_page (int, optional): Number of integrations to fetch per page.
1601
- Defaults to 50.
1742
+ per_page: Number of integrations to fetch per page.
1743
+ Defaults to 50. Usually there is no reason to change this.
1602
1744
 
1603
1745
  Yields:
1604
1746
  Iterator[SlackIntegration]: An iterator of Slack integrations.
@@ -1624,8 +1766,434 @@ class Api:
1624
1766
  """
1625
1767
  from wandb.apis.public.integrations import SlackIntegrations
1626
1768
 
1627
- entity = entity or self.default_entity
1628
- params = {"entityName": entity, "includeSlack": True}
1769
+ params = {"entityName": entity or self.default_entity}
1629
1770
  return SlackIntegrations(
1630
1771
  client=self.client, variables=params, per_page=per_page
1631
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