Skip to content

Commit 097c78b

Browse files
committed
python(feat): Add support for tags to Rules
1 parent d54c304 commit 097c78b

3 files changed

Lines changed: 110 additions & 4 deletions

File tree

python/lib/sift_py/rule/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class RuleConfig(AsJson):
2424
- `channel_references`: Reference to channel. If an expression is "$1 < 10", then "$1" is the reference and thus should the key in the dict.
2525
- `rule_client_key`: User defined unique string that uniquely identifies this rule.
2626
- `asset_names`: A list of asset names that this rule should be applied to. ONLY VALID if defining rules outside of a telemetry config.
27-
- `tag_names`: A list of asset names that this rule should be applied to. ONLY VALID if defining rules outside of a telemetry config.
27+
- `tag_names`: A list of asset tags that this rule should be applied to. ONLY VALID if defining rules outside of a telemetry config.
2828
- `contextual_channels`: A list of channel names that provide context but aren't directly used in the expression.
2929
- `is_external`: If this is an external rule.
3030
- `is_live`: If set to True then this rule will be evaluated on live data, otherwise live rule evaluation will be disabled.
@@ -38,6 +38,7 @@ class RuleConfig(AsJson):
3838
channel_references: List[ExpressionChannelReference]
3939
rule_client_key: Optional[str]
4040
asset_names: List[str]
41+
tag_names: List[str]
4142
contextual_channels: List[str]
4243
is_external: bool
4344
is_live: bool
@@ -65,6 +66,7 @@ def __init__(
6566

6667
self.name = name
6768
self.asset_names = asset_names or []
69+
self.tag_names = tag_names or []
6870
self.action = action
6971
self.rule_client_key = rule_client_key
7072
self.description = description

python/lib/sift_py/rule/service.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
UpdateRuleRequest,
3636
)
3737
from sift.rules.v1.rules_pb2_grpc import RuleServiceStub
38+
from sift.tags.v2.tags_pb2 import Tag, TagType
39+
from sift.tags.v2.tags_pb2_grpc import TagServiceStub
3840
from sift.users.v2.users_pb2_grpc import UserServiceStub
3941

4042
from sift_py._internal.cel import cel_in
@@ -55,6 +57,7 @@
5557
RuleActionKind,
5658
RuleConfig,
5759
)
60+
from sift_py.tag._internal.shared import list_tags_impl
5861
from sift_py.yaml.rule import load_rule_modules
5962

6063

@@ -72,13 +75,15 @@ class RuleService:
7275
_channel_service_stub: ChannelServiceStub
7376
_rule_service_stub: RuleServiceStub
7477
_user_service_stub: UserServiceStub
78+
_tag_service_stub: TagServiceStub
7579
_enable_caching: bool
7680

7781
def __init__(self, channel: SiftChannel, enable_caching=False):
7882
self._asset_service_stub = AssetServiceStub(channel)
7983
self._channel_service_stub = ChannelServiceStub(channel)
8084
self._rule_service_stub = RuleServiceStub(channel)
8185
self._user_service_stub = UserServiceStub(channel)
86+
self._tag_service_stub = TagServiceStub(channel)
8287
self._enable_caching = enable_caching
8388

8489
def load_rules_from_yaml(
@@ -244,6 +249,7 @@ def _parse_rules_from_yaml(
244249
channel_references=rule_channel_references,
245250
contextual_channels=contextual_channels,
246251
asset_names=rule_yaml.get("asset_names", []),
252+
tag_names=rule_yaml.get("tag_names", []),
247253
sub_expressions=subexpr,
248254
is_external=rule_yaml.get("is_external", False),
249255
is_live=rule_yaml.get("is_live", False),
@@ -402,8 +408,17 @@ def _update_req_from_rule_config(
402408
"See `sift_py.rule.config.RuleAction` for available actions."
403409
)
404410

405-
# TODO: once we have TagService_ListTags we can do asset-agnostic rules via tags
406411
assets = self._get_assets(names=config.asset_names) if config.asset_names else None
412+
asset_tags = (
413+
self._get_tags(names=config.tag_names, tag_type=TagType.TAG_TYPE_ASSET)
414+
if config.tag_names
415+
else None
416+
)
417+
annotation_tags = list_tags_impl(
418+
self._tag_service_stub,
419+
names=[tag for tag in config.action.tags],
420+
tag_type=TagType.TAG_TYPE_ANNOTATION,
421+
)
407422

408423
actions = []
409424
if config.action.kind() == RuleActionKind.NOTIFICATION:
@@ -431,7 +446,7 @@ def _update_req_from_rule_config(
431446
annotation=AnnotationActionConfiguration(
432447
assigned_to_user_id=user_id,
433448
annotation_type=AnnotationType.ANNOTATION_TYPE_DATA_REVIEW,
434-
# tag_ids=config.action.tags, # TODO: Requires TagService
449+
tag_ids=annotation_tags,
435450
)
436451
),
437452
)
@@ -442,7 +457,7 @@ def _update_req_from_rule_config(
442457
configuration=RuleActionConfiguration(
443458
annotation=AnnotationActionConfiguration(
444459
annotation_type=AnnotationType.ANNOTATION_TYPE_PHASE,
445-
# tag_ids=config.action.tags, # TODO: Requires TagService
460+
tag_ids=annotation_tags,
446461
)
447462
),
448463
)
@@ -523,6 +538,7 @@ def _update_req_from_rule_config(
523538
],
524539
asset_configuration=RuleAssetConfiguration(
525540
asset_ids=[asset.asset_id for asset in assets] if assets else None,
541+
tag_ids=[tag.tag_id for tag in asset_tags] if asset_tags else None,
526542
),
527543
contextual_channels=ContextualChannels(channels=contextual_channel_names),
528544
is_external=config.is_external,
@@ -574,6 +590,12 @@ def get_rule(self, rule: str) -> Optional[RuleConfig]:
574590
)
575591
asset_names = [asset.name for asset in assets]
576592

593+
asset_tags = self._get_tags(
594+
ids=[tag_id for tag_id in rule_pb.asset_configuration.tag_ids],
595+
tag_type=TagType.TAG_TYPE_ASSET,
596+
)
597+
asset_tag_names = [tag.name for tag in asset_tags]
598+
577599
contextual_channels = []
578600
for channel_ref in rule_pb.contextual_channels.channels:
579601
contextual_channels.append(channel_ref.name)
@@ -585,6 +607,7 @@ def get_rule(self, rule: str) -> Optional[RuleConfig]:
585607
channel_references=channel_references, # type: ignore
586608
contextual_channels=contextual_channels,
587609
asset_names=asset_names,
610+
tag_names=asset_tag_names,
588611
action=action,
589612
expression=expression,
590613
)
@@ -616,6 +639,14 @@ def _get_assets(self, names: List[str] = [], ids: List[str] = []) -> List[Asset]
616639
else:
617640
return list_assets_impl(self._asset_service_stub, names, ids)
618641

642+
def _get_tags(
643+
self, names: List[str] = [], ids: List[str] = [], tag_type: Optional[TagType] = None
644+
) -> List[Tag]:
645+
if self._enable_caching:
646+
return self._get_tags_cached(tuple(sorted(names)), tuple(sorted(ids)), tag_type)
647+
else:
648+
return list_tags_impl(self._tag_service_stub, names, ids, tag_type)
649+
619650
def _get_channels(self, filter: str) -> List[ChannelPb]:
620651
if self._enable_caching:
621652
return self._get_channels_cached(filter)
@@ -632,6 +663,12 @@ def _get_active_users(self, filter: str) -> List[UserPb]:
632663
def _get_assets_cached(self, names: Tuple[str], ids: Tuple[str]) -> List[Asset]:
633664
return list_assets_impl(self._asset_service_stub, names, ids)
634665

666+
@cache
667+
def _get_tags_cached(
668+
self, names: Tuple[str], ids: Tuple[str], tag_type: Optional[TagType] = None
669+
) -> List[Asset]:
670+
return list_tags_impl(self._tag_service_stub, names, ids, tag_type)
671+
635672
@cache
636673
def _get_channels_cached(self, filter: str) -> List[ChannelPb]:
637674
return get_channels(channel_service=self._channel_service_stub, filter=filter)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from typing import List, Optional, Tuple, Union, cast
2+
3+
from sift.tags.v2.tags_pb2 import ListTagsRequest, ListTagsResponse, Tag, TagType
4+
from sift.tags.v2.tags_pb2_grpc import TagServiceStub
5+
from sift_py._internal.cel import cel_in
6+
7+
8+
def list_tags_impl(
9+
tag_service_stub: TagServiceStub,
10+
names: Optional[Union[Tuple[str], List[str]]] = None,
11+
ids: Optional[Union[Tuple[str], List[str]]] = None,
12+
tag_type: Optional[TagType] = None,
13+
) -> List[Tag]:
14+
"""
15+
Lists tags in an organization.
16+
17+
Args:
18+
tag_service_stub: The tag service stub to use.
19+
names: Optional collection of names to filter by.
20+
ids: Optional collection of IDs to filter by.
21+
tag_type: Optional tag type to filter by.
22+
23+
Returns:
24+
A list of tags matching the criteria.
25+
"""
26+
27+
def get_tags_with_filter(
28+
tag_service_stub: TagServiceStub, cel_filter: str, tag_type: Optional[TagType]
29+
) -> List[Tag]:
30+
tags: List[Tag] = []
31+
next_page_token = ""
32+
while True:
33+
req_kwargs = {
34+
"filter": cel_filter,
35+
"page_size": 1_000,
36+
"page_token": next_page_token,
37+
}
38+
if tag_type is not None:
39+
req_kwargs["tag_type"] = tag_type
40+
req = ListTagsRequest(**req_kwargs)
41+
res = cast(ListTagsResponse, tag_service_stub.ListTags(req))
42+
tags.extend(res.tags)
43+
44+
if not res.next_page_token:
45+
break
46+
next_page_token = res.next_page_token
47+
48+
return tags
49+
50+
if names is None:
51+
names = []
52+
if ids is None:
53+
ids = []
54+
55+
results: List[Tag] = []
56+
if names:
57+
names_cel = cel_in("name", names)
58+
results.append(get_tags_with_filter(tag_service_stub, names_cel, tag_type))
59+
if ids:
60+
ids_cel = cel_in("tag_id", ids)
61+
results.append(get_tags_with_filter(tag_service_stub, ids_cel, tag_type))
62+
if not names and not ids:
63+
# If no filter, but tag_type is specified, fetch all tags of that type
64+
if tag_type is not None:
65+
results.append(get_tags_with_filter(tag_service_stub, "", tag_type))
66+
67+
return results

0 commit comments

Comments
 (0)