wandb 0.19.10__py3-none-macosx_11_0_arm64.whl → 0.19.11__py3-none-macosx_11_0_arm64.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.
- wandb/__init__.py +1 -1
- wandb/__init__.pyi +3 -3
- wandb/_pydantic/__init__.py +2 -3
- wandb/_pydantic/base.py +11 -31
- wandb/_pydantic/utils.py +8 -1
- wandb/_pydantic/v1_compat.py +3 -3
- wandb/apis/public/api.py +590 -22
- wandb/apis/public/artifacts.py +13 -5
- wandb/apis/public/automations.py +1 -1
- wandb/apis/public/integrations.py +22 -10
- wandb/apis/public/registries/__init__.py +0 -0
- wandb/apis/public/registries/_freezable_list.py +179 -0
- wandb/apis/public/{registries.py → registries/registries_search.py} +22 -129
- wandb/apis/public/registries/registry.py +357 -0
- wandb/apis/public/registries/utils.py +140 -0
- wandb/apis/public/runs.py +58 -56
- wandb/automations/__init__.py +16 -24
- wandb/automations/_filters/expressions.py +12 -10
- wandb/automations/_filters/operators.py +10 -19
- wandb/automations/_filters/run_metrics.py +231 -82
- wandb/automations/_generated/__init__.py +27 -34
- wandb/automations/_generated/create_automation.py +17 -0
- wandb/automations/_generated/delete_automation.py +17 -0
- wandb/automations/_generated/fragments.py +40 -25
- wandb/automations/_generated/{get_triggers.py → get_automations.py} +5 -5
- wandb/automations/_generated/{get_triggers_by_entity.py → get_automations_by_entity.py} +7 -5
- wandb/automations/_generated/operations.py +35 -98
- wandb/automations/_generated/update_automation.py +17 -0
- wandb/automations/_utils.py +178 -64
- wandb/automations/_validators.py +94 -2
- wandb/automations/actions.py +113 -98
- wandb/automations/automations.py +47 -69
- wandb/automations/events.py +139 -87
- wandb/automations/integrations.py +23 -4
- wandb/automations/scopes.py +22 -20
- wandb/bin/gpu_stats +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/env.py +11 -0
- wandb/old/settings.py +4 -1
- wandb/proto/v3/wandb_internal_pb2.py +240 -236
- wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v4/wandb_internal_pb2.py +236 -236
- wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v5/wandb_internal_pb2.py +236 -236
- wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v6/wandb_internal_pb2.py +236 -236
- wandb/proto/v6/wandb_telemetry_pb2.py +10 -10
- wandb/sdk/artifacts/_generated/__init__.py +42 -1
- wandb/sdk/artifacts/_generated/add_aliases.py +21 -0
- wandb/sdk/artifacts/_generated/delete_aliases.py +21 -0
- wandb/sdk/artifacts/_generated/fetch_linked_artifacts.py +67 -0
- wandb/sdk/artifacts/_generated/fragments.py +35 -0
- wandb/sdk/artifacts/_generated/input_types.py +12 -0
- wandb/sdk/artifacts/_generated/operations.py +101 -0
- wandb/sdk/artifacts/_generated/update_artifact.py +26 -0
- wandb/sdk/artifacts/_graphql_fragments.py +1 -0
- wandb/sdk/artifacts/_validators.py +120 -1
- wandb/sdk/artifacts/artifact.py +380 -203
- wandb/sdk/artifacts/artifact_file_cache.py +4 -6
- wandb/sdk/artifacts/artifact_manifest_entry.py +11 -2
- wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +182 -1
- wandb/sdk/artifacts/storage_policy.py +3 -0
- wandb/sdk/data_types/video.py +46 -32
- wandb/sdk/interface/interface.py +2 -3
- wandb/sdk/internal/internal_api.py +21 -31
- wandb/sdk/internal/sender.py +5 -2
- wandb/sdk/launch/sweeps/utils.py +8 -0
- wandb/sdk/projects/_generated/__init__.py +47 -0
- wandb/sdk/projects/_generated/delete_project.py +22 -0
- wandb/sdk/projects/_generated/enums.py +4 -0
- wandb/sdk/projects/_generated/fetch_registry.py +22 -0
- wandb/sdk/projects/_generated/fragments.py +41 -0
- wandb/sdk/projects/_generated/input_types.py +13 -0
- wandb/sdk/projects/_generated/operations.py +88 -0
- wandb/sdk/projects/_generated/rename_project.py +27 -0
- wandb/sdk/projects/_generated/upsert_registry_project.py +27 -0
- wandb/sdk/service/service.py +9 -1
- wandb/sdk/wandb_init.py +32 -5
- wandb/sdk/wandb_run.py +37 -9
- wandb/sdk/wandb_settings.py +6 -7
- wandb/sdk/wandb_setup.py +12 -0
- wandb/util.py +7 -3
- {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/METADATA +1 -1
- {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/RECORD +87 -70
- wandb/automations/_generated/create_filter_trigger.py +0 -21
- wandb/automations/_generated/delete_trigger.py +0 -19
- wandb/automations/_generated/update_filter_trigger.py +0 -21
- {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/WHEEL +0 -0
- {wandb-0.19.10.dist-info → wandb-0.19.11.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
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 =
|
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
|
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
|
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
|
-
|
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
|
-
"""
|
1695
|
+
"""Returns an iterator of webhook integrations for an entity.
|
1553
1696
|
|
1554
1697
|
Args:
|
1555
|
-
entity
|
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
|
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
|
-
|
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,
|
1734
|
+
self, *, entity: Optional[str] = None, per_page: int = 50
|
1593
1735
|
) -> Iterator["SlackIntegration"]:
|
1594
|
-
"""
|
1736
|
+
"""Returns an iterator of Slack integrations for an entity.
|
1595
1737
|
|
1596
1738
|
Args:
|
1597
|
-
entity
|
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
|
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
|
-
|
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
|