better-aws-tags 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- better_aws_tags-0.3.1/.github/workflows/release.yml +23 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/Makefile +1 -1
- better_aws_tags-0.3.1/PKG-INFO +20 -0
- better_aws_tags-0.3.1/README.md +11 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/src/better_aws_tags/__init__.py +1 -1
- better_aws_tags-0.3.1/src/better_aws_tags/arnparse.py +139 -0
- better_aws_tags-0.3.1/src/better_aws_tags/handlers.py +134 -0
- better_aws_tags-0.3.1/src/better_aws_tags/tags.py +45 -0
- better_aws_tags-0.3.1/tests/test_arnparse.py +363 -0
- better_aws_tags-0.2.0/.claude/settings.local.json +0 -16
- better_aws_tags-0.2.0/PKG-INFO +0 -18
- better_aws_tags-0.2.0/README.md +0 -9
- better_aws_tags-0.2.0/src/better_aws_tags/tags.py +0 -39
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/.gitignore +0 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/.python-version +0 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/pyproject.toml +0 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/tests/test_import.py +0 -0
- {better_aws_tags-0.2.0 → better_aws_tags-0.3.1}/uv.lock +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
|
|
19
|
+
- name: Build package
|
|
20
|
+
run: uv build
|
|
21
|
+
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: better-aws-tags
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A unified Python interface for managing AWS resource tags
|
|
5
|
+
Author-email: Andrey Gubarev <andrey@andreygubarev.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: boto3~=1.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# better-aws-tags
|
|
11
|
+
|
|
12
|
+
A unified Python interface for managing AWS resource tags.
|
|
13
|
+
|
|
14
|
+
AWS provides multiple tagging APIs across services, including the Resource Groups Tagging API and service-specific implementations such as EC2, S3, and IAM. This library abstracts these differences behind a single API that accepts any valid ARN and handles service routing, tag validation, and error reporting automatically.
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import better_aws_tags as bats
|
|
20
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# better-aws-tags
|
|
2
|
+
|
|
3
|
+
A unified Python interface for managing AWS resource tags.
|
|
4
|
+
|
|
5
|
+
AWS provides multiple tagging APIs across services, including the Resource Groups Tagging API and service-specific implementations such as EC2, S3, and IAM. This library abstracts these differences behind a single API that accepts any valid ARN and handles service routing, tag validation, and error reporting automatically.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import better_aws_tags as bats
|
|
11
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""ARN parsing utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
REGEX_RESOURCE_TYPE = re.compile(r"^[a-zA-Z0-9-]+$")
|
|
7
|
+
|
|
8
|
+
class ResourceNotFound(Exception):
|
|
9
|
+
def __init__(self, service, resource):
|
|
10
|
+
super().__init__(f"Cannot determine resource format for service {service}: {resource}")
|
|
11
|
+
|
|
12
|
+
class ARN(NamedTuple):
|
|
13
|
+
partition: str
|
|
14
|
+
service: str
|
|
15
|
+
region: str
|
|
16
|
+
account: str
|
|
17
|
+
resource_type: str | None
|
|
18
|
+
resource_id: str
|
|
19
|
+
resource_name: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Resource(NamedTuple):
|
|
23
|
+
resource_type: str | None
|
|
24
|
+
resource_id: str
|
|
25
|
+
resource_name: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_resource(service, resource):
|
|
29
|
+
separators = resource.count("/") + resource.count(":")
|
|
30
|
+
segments = [s for segment in resource.split("/") for s in segment.split(":")]
|
|
31
|
+
prefix = segments[0]
|
|
32
|
+
suffix = resource[len(prefix) + 1:]
|
|
33
|
+
|
|
34
|
+
if service == "apigateway":
|
|
35
|
+
if separators == 2:
|
|
36
|
+
return Resource(segments[1], segments[2], segments[2])
|
|
37
|
+
else:
|
|
38
|
+
raise ResourceNotFound(service, resource)
|
|
39
|
+
elif service == "autoscaling":
|
|
40
|
+
if segments[0] == "autoScalingGroup" and segments[2] == "autoScalingGroupName":
|
|
41
|
+
return Resource(segments[0], segments[1], segments[3])
|
|
42
|
+
else:
|
|
43
|
+
raise ResourceNotFound(service, resource)
|
|
44
|
+
elif service == "cloudformation":
|
|
45
|
+
if separators == 2:
|
|
46
|
+
return Resource(segments[0], segments[2], segments[1])
|
|
47
|
+
else:
|
|
48
|
+
raise ResourceNotFound(service, resource)
|
|
49
|
+
elif service == "cloudwatch":
|
|
50
|
+
if prefix == "alarm":
|
|
51
|
+
return Resource(prefix, suffix, suffix)
|
|
52
|
+
else:
|
|
53
|
+
raise ResourceNotFound(service, resource)
|
|
54
|
+
elif service == "elasticloadbalancing":
|
|
55
|
+
if prefix == "loadbalancer":
|
|
56
|
+
if separators == 3:
|
|
57
|
+
return Resource(f"{segments[0]}/{segments[1]}", segments[3], segments[2])
|
|
58
|
+
elif separators == 1:
|
|
59
|
+
return Resource(segments[0], segments[1], segments[1])
|
|
60
|
+
else:
|
|
61
|
+
raise ResourceNotFound(service, resource)
|
|
62
|
+
elif prefix == "targetgroup":
|
|
63
|
+
if separators == 2:
|
|
64
|
+
return Resource(prefix, segments[2], segments[1])
|
|
65
|
+
else:
|
|
66
|
+
raise ResourceNotFound(service, resource)
|
|
67
|
+
else:
|
|
68
|
+
raise ResourceNotFound(service, resource)
|
|
69
|
+
elif service == "logs":
|
|
70
|
+
if prefix == "log-group":
|
|
71
|
+
return Resource(prefix, suffix, suffix)
|
|
72
|
+
else:
|
|
73
|
+
raise ResourceNotFound(service, resource)
|
|
74
|
+
elif service == "secretsmanager":
|
|
75
|
+
if prefix == "secret":
|
|
76
|
+
return Resource(prefix, suffix, suffix)
|
|
77
|
+
else:
|
|
78
|
+
raise ResourceNotFound(service, resource)
|
|
79
|
+
elif service == "ssm":
|
|
80
|
+
if prefix == "parameter":
|
|
81
|
+
return Resource(prefix, "/" + suffix, "/" + suffix)
|
|
82
|
+
else:
|
|
83
|
+
raise ResourceNotFound(service, resource)
|
|
84
|
+
elif service == "wafv2":
|
|
85
|
+
if prefix == "global":
|
|
86
|
+
return Resource(f"{segments[0]}/{segments[1]}", segments[2], segments[2])
|
|
87
|
+
elif prefix == "regional":
|
|
88
|
+
return Resource(f"{segments[0]}/{segments[1]}", segments[3], segments[2])
|
|
89
|
+
else:
|
|
90
|
+
raise ResourceNotFound(service, resource)
|
|
91
|
+
elif separators == 1:
|
|
92
|
+
return Resource(segments[0], segments[1], segments[1])
|
|
93
|
+
elif separators == 0:
|
|
94
|
+
return Resource(None, segments[0], segments[0])
|
|
95
|
+
else:
|
|
96
|
+
raise ResourceNotFound(service, resource)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def arnparse(arn):
|
|
100
|
+
"""Parse an ARN string into components."""
|
|
101
|
+
parts = arn.split(":", 5)
|
|
102
|
+
if len(parts) != 6:
|
|
103
|
+
raise ValueError(f"Invalid ARN format: {arn}")
|
|
104
|
+
|
|
105
|
+
if parts[0] != "arn":
|
|
106
|
+
raise ValueError(f"ARN must start with 'arn': {arn}")
|
|
107
|
+
|
|
108
|
+
_, partition, service, region, account, resource = parts
|
|
109
|
+
|
|
110
|
+
# Global services - no region, no account
|
|
111
|
+
if service in {"s3", "route53"}:
|
|
112
|
+
if region or account:
|
|
113
|
+
raise ValueError(f"Service {service} must have empty region and account: {arn}")
|
|
114
|
+
|
|
115
|
+
# Global services - no region, has account
|
|
116
|
+
if service in {"cloudfront", "iam"}:
|
|
117
|
+
if region:
|
|
118
|
+
raise ValueError(f"Service {service} must have empty region: {arn}")
|
|
119
|
+
if not account:
|
|
120
|
+
raise ValueError(f"Service {service} must have account: {arn}")
|
|
121
|
+
|
|
122
|
+
# Regional services - has region, no account
|
|
123
|
+
if service in {"apigateway"}:
|
|
124
|
+
if not region:
|
|
125
|
+
raise ValueError(f"Service {service} must have region: {arn}")
|
|
126
|
+
if account:
|
|
127
|
+
raise ValueError(f"Service {service} must have empty account: {arn}")
|
|
128
|
+
|
|
129
|
+
resource = find_resource(service, resource)
|
|
130
|
+
|
|
131
|
+
return ARN(
|
|
132
|
+
partition=partition,
|
|
133
|
+
service=service,
|
|
134
|
+
region=region,
|
|
135
|
+
account=account,
|
|
136
|
+
resource_type=resource.resource_type,
|
|
137
|
+
resource_id=resource.resource_id,
|
|
138
|
+
resource_name=resource.resource_name,
|
|
139
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
|
|
5
|
+
from .arnparse import arnparse
|
|
6
|
+
|
|
7
|
+
class Handler(ABC):
|
|
8
|
+
@classmethod
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def get_tags(cls, session, arns):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def set_tags(cls, session, arns, tags):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IAMHandler(Handler):
|
|
20
|
+
"""Handler for IAM resources using dedicated IAM tagging API."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_tags(cls, session, arns):
|
|
24
|
+
if not arns:
|
|
25
|
+
raise ValueError("At least one ARN must be provided to get_tags.")
|
|
26
|
+
|
|
27
|
+
client = session.client("iam")
|
|
28
|
+
result = {}
|
|
29
|
+
|
|
30
|
+
for arn in arns:
|
|
31
|
+
arn_data = arnparse(arn)
|
|
32
|
+
resource_type = arn_data.resource_type
|
|
33
|
+
resource_name = arn_data.resource_name
|
|
34
|
+
|
|
35
|
+
if resource_type == "role":
|
|
36
|
+
response = client.list_role_tags(RoleName=resource_name)
|
|
37
|
+
elif resource_type == "user":
|
|
38
|
+
response = client.list_user_tags(UserName=resource_name)
|
|
39
|
+
elif resource_type == "policy":
|
|
40
|
+
response = client.list_policy_tags(PolicyArn=arn)
|
|
41
|
+
elif resource_type == "instance-profile":
|
|
42
|
+
response = client.list_instance_profile_tags(InstanceProfileName=resource_name)
|
|
43
|
+
else:
|
|
44
|
+
response = None
|
|
45
|
+
|
|
46
|
+
if response:
|
|
47
|
+
result[arn] = {t["Key"]: t["Value"] for t in response["Tags"]}
|
|
48
|
+
else:
|
|
49
|
+
result[arn] = None
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def set_tags(cls, session, arns, tags):
|
|
55
|
+
if not arns:
|
|
56
|
+
raise ValueError("At least one ARN must be provided to set_tags.")
|
|
57
|
+
|
|
58
|
+
client = session.client("iam")
|
|
59
|
+
result = {}
|
|
60
|
+
payload = [{"Key": k, "Value": v} for k, v in tags.items()]
|
|
61
|
+
|
|
62
|
+
for arn in arns:
|
|
63
|
+
arn_data = arnparse(arn)
|
|
64
|
+
resource_type = arn_data.resource_type
|
|
65
|
+
resource_name = arn_data.resource_name
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if resource_type == "role":
|
|
69
|
+
response = client.tag_role(RoleName=resource_name, Tags=payload)
|
|
70
|
+
elif resource_type == "user":
|
|
71
|
+
response = client.tag_user(UserName=resource_name, Tags=payload)
|
|
72
|
+
elif resource_type == "policy":
|
|
73
|
+
response = client.tag_policy(PolicyArn=arn, Tags=payload)
|
|
74
|
+
elif resource_type == "instance-profile":
|
|
75
|
+
response = client.tag_instance_profile(InstanceProfileName=resource_name, Tags=payload)
|
|
76
|
+
else:
|
|
77
|
+
result[arn] = None
|
|
78
|
+
continue
|
|
79
|
+
except client.exceptions.NoSuchEntityException:
|
|
80
|
+
result[arn] = None
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
result[arn] = response["ResponseMetadata"]["HTTPStatusCode"] == 200
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TaggingAPIHandler(Handler):
|
|
89
|
+
@classmethod
|
|
90
|
+
def get_tags(cls, session, arns):
|
|
91
|
+
if not arns:
|
|
92
|
+
raise ValueError("At least one ARN must be provided to get_tags.")
|
|
93
|
+
|
|
94
|
+
client = session.client("resourcegroupstaggingapi")
|
|
95
|
+
result = {}
|
|
96
|
+
|
|
97
|
+
# https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html
|
|
98
|
+
batch_size = 100
|
|
99
|
+
for i in range(0, len(arns), batch_size):
|
|
100
|
+
batch = arns[i : i + batch_size]
|
|
101
|
+
response = client.get_resources(ResourceARNList=batch)
|
|
102
|
+
response = response["ResourceTagMappingList"]
|
|
103
|
+
for arn in batch:
|
|
104
|
+
if arn in response:
|
|
105
|
+
result[arn] = {
|
|
106
|
+
t["Key"]: t["Value"] for t in response[arn].get("Tags", [])
|
|
107
|
+
}
|
|
108
|
+
else:
|
|
109
|
+
result[arn] = None
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def set_tags(cls, session, arns, tags):
|
|
114
|
+
if not arns:
|
|
115
|
+
raise ValueError("At least one ARN must be provided to set_tags.")
|
|
116
|
+
|
|
117
|
+
if len(tags) == 0:
|
|
118
|
+
raise ValueError("At least one tag must be provided to set_tags.")
|
|
119
|
+
|
|
120
|
+
if len(tags) > 50:
|
|
121
|
+
raise ValueError("A maximum of 50 tags can be set at once.")
|
|
122
|
+
|
|
123
|
+
client = session.client("resourcegroupstaggingapi")
|
|
124
|
+
result = {}
|
|
125
|
+
|
|
126
|
+
# https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_TagResources.html
|
|
127
|
+
batch_size = 20
|
|
128
|
+
for i in range(0, len(arns), batch_size):
|
|
129
|
+
batch = arns[i : i + batch_size]
|
|
130
|
+
response = client.tag_resources(ResourceARNList=batch, Tags=tags)
|
|
131
|
+
for arn in batch:
|
|
132
|
+
result[arn] = arn not in response["FailedResourcesMap"]
|
|
133
|
+
|
|
134
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""AWS resource tag operations."""
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
|
|
5
|
+
from .arnparse import arnparse
|
|
6
|
+
from .handlers import IAMHandler, TaggingAPIHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_tags(arns, session=None):
|
|
10
|
+
"""Get tags for AWS resources."""
|
|
11
|
+
session = session or boto3.Session()
|
|
12
|
+
handlers = dispatch(arns)
|
|
13
|
+
results = {}
|
|
14
|
+
for handler, handler_arns in handlers.items():
|
|
15
|
+
r = handler.get_tags(session, handler_arns)
|
|
16
|
+
results.update(r)
|
|
17
|
+
return results
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_tags(arns, tags, session=None):
|
|
21
|
+
"""Set tags on AWS resources."""
|
|
22
|
+
session = session or boto3.Session()
|
|
23
|
+
handlers = dispatch(arns)
|
|
24
|
+
results = {}
|
|
25
|
+
for handler, handler_arns in handlers.items():
|
|
26
|
+
r = handler.set_tags(session, handler_arns, tags)
|
|
27
|
+
results.update(r)
|
|
28
|
+
return results
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dispatch(arns):
|
|
32
|
+
"""Group ARNs by handler."""
|
|
33
|
+
handler_to_arns = {}
|
|
34
|
+
for arn in arns:
|
|
35
|
+
arn_data = arnparse(arn)
|
|
36
|
+
|
|
37
|
+
if arn_data.service == "iam":
|
|
38
|
+
handler = IAMHandler
|
|
39
|
+
else:
|
|
40
|
+
handler = TaggingAPIHandler
|
|
41
|
+
|
|
42
|
+
if handler not in handler_to_arns:
|
|
43
|
+
handler_to_arns[handler] = []
|
|
44
|
+
handler_to_arns[handler].append(arn)
|
|
45
|
+
return handler_to_arns
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Tests for ARN parsing."""
|
|
2
|
+
|
|
3
|
+
from better_aws_tags.arnparse import arnparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_arnparse_acm():
|
|
7
|
+
result = arnparse("arn:aws:acm:us-east-1:012345678901:certificate/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
8
|
+
assert result.resource_type == "certificate"
|
|
9
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
10
|
+
assert result.resource_name == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_arnparse_apigateway():
|
|
14
|
+
result = arnparse("arn:aws:apigateway:us-east-1::/restapis/abc123def4")
|
|
15
|
+
assert result.resource_type == "restapis"
|
|
16
|
+
assert result.resource_id == "abc123def4"
|
|
17
|
+
assert result.resource_name == "abc123def4"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_arnparse_athena():
|
|
21
|
+
result = arnparse("arn:aws:athena:us-east-1:012345678901:workgroup/workgroup1")
|
|
22
|
+
assert result.resource_type == "workgroup"
|
|
23
|
+
assert result.resource_id == "workgroup1"
|
|
24
|
+
assert result.resource_name == "workgroup1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_arnparse_autoscaling():
|
|
28
|
+
result = arnparse("arn:aws:autoscaling:us-east-1:012345678901:autoScalingGroup:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:autoScalingGroupName/asg1")
|
|
29
|
+
assert result.resource_type == "autoScalingGroup"
|
|
30
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
31
|
+
assert result.resource_name == "asg1"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_arnparse_backup():
|
|
35
|
+
result = arnparse("arn:aws:backup:us-east-1:012345678901:backup-vault:vault1")
|
|
36
|
+
assert result.resource_type == "backup-vault"
|
|
37
|
+
assert result.resource_id == "vault1"
|
|
38
|
+
assert result.resource_name == "vault1"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_arnparse_cloudformation():
|
|
42
|
+
result = arnparse("arn:aws:cloudformation:us-east-1:012345678901:stack/stack1/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
43
|
+
assert result.resource_type == "stack"
|
|
44
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
45
|
+
assert result.resource_name == "stack1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_arnparse_cloudfront():
|
|
49
|
+
result = arnparse("arn:aws:cloudfront::012345678901:distribution/ABCDEFGHIJKLMN")
|
|
50
|
+
assert result.resource_type == "distribution"
|
|
51
|
+
assert result.resource_id == "ABCDEFGHIJKLMN"
|
|
52
|
+
assert result.resource_name == "ABCDEFGHIJKLMN"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_arnparse_cloudtrail():
|
|
56
|
+
result = arnparse("arn:aws:cloudtrail:us-east-1:012345678901:trail/trailname1")
|
|
57
|
+
assert result.resource_type == "trail"
|
|
58
|
+
assert result.resource_id == "trailname1"
|
|
59
|
+
assert result.resource_name == "trailname1"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_arnparse_cloudwatch():
|
|
63
|
+
result = arnparse("arn:aws:cloudwatch:us-east-1:012345678901:alarm:CPU Utilization - High Warning - service-production-1")
|
|
64
|
+
assert result.resource_type == "alarm"
|
|
65
|
+
assert result.resource_id == "CPU Utilization - High Warning - service-production-1"
|
|
66
|
+
assert result.resource_name == "CPU Utilization - High Warning - service-production-1"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_arnparse_codebuild():
|
|
70
|
+
result = arnparse("arn:aws:codebuild:us-east-1:012345678901:project/build_image")
|
|
71
|
+
assert result.resource_type == "project"
|
|
72
|
+
assert result.resource_id == "build_image"
|
|
73
|
+
assert result.resource_name == "build_image"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_arnparse_codecommit():
|
|
77
|
+
result = arnparse("arn:aws:codecommit:us-east-1:012345678901:repository1")
|
|
78
|
+
assert result.resource_type is None
|
|
79
|
+
assert result.resource_id == "repository1"
|
|
80
|
+
assert result.resource_name == "repository1"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_arnparse_codepipeline():
|
|
84
|
+
result = arnparse("arn:aws:codepipeline:us-east-1:012345678901:deploy_pipeline")
|
|
85
|
+
assert result.resource_type is None
|
|
86
|
+
assert result.resource_id == "deploy_pipeline"
|
|
87
|
+
assert result.resource_name == "deploy_pipeline"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_arnparse_datasync():
|
|
91
|
+
result = arnparse("arn:aws:datasync:us-east-1:012345678901:task/task-0123456789abcdef0")
|
|
92
|
+
assert result.resource_type == "task"
|
|
93
|
+
assert result.resource_id == "task-0123456789abcdef0"
|
|
94
|
+
assert result.resource_name == "task-0123456789abcdef0"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_arnparse_dynamodb():
|
|
98
|
+
result = arnparse("arn:aws:dynamodb:us-east-1:012345678901:table/table1")
|
|
99
|
+
assert result.resource_type == "table"
|
|
100
|
+
assert result.resource_id == "table1"
|
|
101
|
+
assert result.resource_name == "table1"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_arnparse_ec2():
|
|
105
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:instance/i-0123456789abcdef0")
|
|
106
|
+
assert result.resource_type == "instance"
|
|
107
|
+
assert result.resource_id == "i-0123456789abcdef0"
|
|
108
|
+
assert result.resource_name == "i-0123456789abcdef0"
|
|
109
|
+
|
|
110
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:volume/vol-0123456789abcdef0")
|
|
111
|
+
assert result.resource_type == "volume"
|
|
112
|
+
assert result.resource_id == "vol-0123456789abcdef0"
|
|
113
|
+
|
|
114
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:snapshot/snap-0123456789abcdef0")
|
|
115
|
+
assert result.resource_type == "snapshot"
|
|
116
|
+
assert result.resource_id == "snap-0123456789abcdef0"
|
|
117
|
+
|
|
118
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:image/ami-0123456789abcdef0")
|
|
119
|
+
assert result.resource_type == "image"
|
|
120
|
+
assert result.resource_id == "ami-0123456789abcdef0"
|
|
121
|
+
|
|
122
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:elastic-ip/eipalloc-0123456789abcdef0")
|
|
123
|
+
assert result.resource_type == "elastic-ip"
|
|
124
|
+
assert result.resource_id == "eipalloc-0123456789abcdef0"
|
|
125
|
+
|
|
126
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:natgateway/nat-0123456789abcdef0")
|
|
127
|
+
assert result.resource_type == "natgateway"
|
|
128
|
+
assert result.resource_id == "nat-0123456789abcdef0"
|
|
129
|
+
|
|
130
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:vpc-endpoint/vpce-0123456789abcdef0")
|
|
131
|
+
assert result.resource_type == "vpc-endpoint"
|
|
132
|
+
assert result.resource_id == "vpce-0123456789abcdef0"
|
|
133
|
+
|
|
134
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:launch-template/lt-0123456789abcdef0")
|
|
135
|
+
assert result.resource_type == "launch-template"
|
|
136
|
+
assert result.resource_id == "lt-0123456789abcdef0"
|
|
137
|
+
|
|
138
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:reserved-instances/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
139
|
+
assert result.resource_type == "reserved-instances"
|
|
140
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
141
|
+
|
|
142
|
+
result = arnparse("arn:aws:ec2:us-east-1:012345678901:spot-instances-request/sir-abcd1234")
|
|
143
|
+
assert result.resource_type == "spot-instances-request"
|
|
144
|
+
assert result.resource_id == "sir-abcd1234"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_arnparse_ecr():
|
|
148
|
+
result = arnparse("arn:aws:ecr:us-east-1:012345678901:repository/web-service")
|
|
149
|
+
assert result.resource_type == "repository"
|
|
150
|
+
assert result.resource_id == "web-service"
|
|
151
|
+
assert result.resource_name == "web-service"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_arnparse_eks():
|
|
155
|
+
result = arnparse("arn:aws:eks:us-east-1:012345678901:cluster/c1")
|
|
156
|
+
assert result.resource_type == "cluster"
|
|
157
|
+
assert result.resource_id == "c1"
|
|
158
|
+
assert result.resource_name == "c1"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_arnparse_elasticache():
|
|
162
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:cluster:redis-cluster-node-001")
|
|
163
|
+
assert result.resource_type == "cluster"
|
|
164
|
+
assert result.resource_id == "redis-cluster-node-001"
|
|
165
|
+
|
|
166
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:parametergroup:redis-cluster-params-redis7")
|
|
167
|
+
assert result.resource_type == "parametergroup"
|
|
168
|
+
assert result.resource_id == "redis-cluster-params-redis7"
|
|
169
|
+
|
|
170
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:replicationgroup:redis-repl-group1")
|
|
171
|
+
assert result.resource_type == "replicationgroup"
|
|
172
|
+
assert result.resource_id == "redis-repl-group1"
|
|
173
|
+
|
|
174
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:reserved-instance:reserved-node-prod1")
|
|
175
|
+
assert result.resource_type == "reserved-instance"
|
|
176
|
+
assert result.resource_id == "reserved-node-prod1"
|
|
177
|
+
|
|
178
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:subnetgroup:cache-subnet-group1")
|
|
179
|
+
assert result.resource_type == "subnetgroup"
|
|
180
|
+
assert result.resource_id == "cache-subnet-group1"
|
|
181
|
+
|
|
182
|
+
result = arnparse("arn:aws:elasticache:us-east-1:012345678901:user:default")
|
|
183
|
+
assert result.resource_type == "user"
|
|
184
|
+
assert result.resource_id == "default"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_arnparse_elasticfilesystem():
|
|
188
|
+
result = arnparse("arn:aws:elasticfilesystem:us-east-1:012345678901:file-system/fs-01234567")
|
|
189
|
+
assert result.resource_type == "file-system"
|
|
190
|
+
assert result.resource_id == "fs-01234567"
|
|
191
|
+
assert result.resource_name == "fs-01234567"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_arnparse_elasticloadbalancing():
|
|
195
|
+
# Classic LB
|
|
196
|
+
result = arnparse("arn:aws:elasticloadbalancing:us-east-1:012345678901:loadbalancer/a0123456789abcdef0123456789abcde")
|
|
197
|
+
assert result.resource_type == "loadbalancer"
|
|
198
|
+
assert result.resource_id == "a0123456789abcdef0123456789abcde"
|
|
199
|
+
|
|
200
|
+
# ALB
|
|
201
|
+
result = arnparse("arn:aws:elasticloadbalancing:us-east-1:012345678901:loadbalancer/app/alb-application-lb-name/0123456789abcdef")
|
|
202
|
+
assert result.resource_type == "loadbalancer/app"
|
|
203
|
+
assert result.resource_id == "0123456789abcdef"
|
|
204
|
+
assert result.resource_name == "alb-application-lb-name"
|
|
205
|
+
|
|
206
|
+
# NLB
|
|
207
|
+
result = arnparse("arn:aws:elasticloadbalancing:us-east-1:012345678901:loadbalancer/net/nlb-network-load-balancer/0123456789abcdef")
|
|
208
|
+
assert result.resource_type == "loadbalancer/net"
|
|
209
|
+
assert result.resource_id == "0123456789abcdef"
|
|
210
|
+
assert result.resource_name == "nlb-network-load-balancer"
|
|
211
|
+
|
|
212
|
+
# Target group
|
|
213
|
+
result = arnparse("arn:aws:elasticloadbalancing:us-east-1:012345678901:targetgroup/target-grp-1/0123456789abcdef")
|
|
214
|
+
assert result.resource_type == "targetgroup"
|
|
215
|
+
assert result.resource_id == "0123456789abcdef"
|
|
216
|
+
assert result.resource_name == "target-grp-1"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_arnparse_es():
|
|
220
|
+
result = arnparse("arn:aws:es:us-east-1:012345678901:domain/search-domain-prod")
|
|
221
|
+
assert result.resource_type == "domain"
|
|
222
|
+
assert result.resource_id == "search-domain-prod"
|
|
223
|
+
assert result.resource_name == "search-domain-prod"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_arnparse_events():
|
|
227
|
+
result = arnparse("arn:aws:events:us-east-1:012345678901:event-bus/default")
|
|
228
|
+
assert result.resource_type == "event-bus"
|
|
229
|
+
assert result.resource_id == "default"
|
|
230
|
+
|
|
231
|
+
result = arnparse("arn:aws:events:us-east-1:012345678901:rule/ScheduledEventRule01")
|
|
232
|
+
assert result.resource_type == "rule"
|
|
233
|
+
assert result.resource_id == "ScheduledEventRule01"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_arnparse_guardduty():
|
|
237
|
+
result = arnparse("arn:aws:guardduty:us-east-1:012345678901:detector/0123456789abcdef0123456789abcdef")
|
|
238
|
+
assert result.resource_type == "detector"
|
|
239
|
+
assert result.resource_id == "0123456789abcdef0123456789abcdef"
|
|
240
|
+
assert result.resource_name == "0123456789abcdef0123456789abcdef"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_arnparse_iam():
|
|
244
|
+
result = arnparse("arn:aws:iam::012345678901:user/admin")
|
|
245
|
+
assert result.resource_type == "user"
|
|
246
|
+
assert result.resource_id == "admin"
|
|
247
|
+
assert result.resource_name == "admin"
|
|
248
|
+
|
|
249
|
+
result = arnparse("arn:aws:iam::012345678901:role/lambda-execution-role")
|
|
250
|
+
assert result.resource_type == "role"
|
|
251
|
+
assert result.resource_id == "lambda-execution-role"
|
|
252
|
+
|
|
253
|
+
result = arnparse("arn:aws:iam::012345678901:policy/custom-policy")
|
|
254
|
+
assert result.resource_type == "policy"
|
|
255
|
+
assert result.resource_id == "custom-policy"
|
|
256
|
+
|
|
257
|
+
result = arnparse("arn:aws:iam::012345678901:group/developers")
|
|
258
|
+
assert result.resource_type == "group"
|
|
259
|
+
assert result.resource_id == "developers"
|
|
260
|
+
|
|
261
|
+
result = arnparse("arn:aws:iam::012345678901:instance-profile/ec2-profile")
|
|
262
|
+
assert result.resource_type == "instance-profile"
|
|
263
|
+
assert result.resource_id == "ec2-profile"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_arnparse_kms():
|
|
267
|
+
result = arnparse("arn:aws:kms:us-east-1:012345678901:key/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
268
|
+
assert result.resource_type == "key"
|
|
269
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
270
|
+
assert result.resource_name == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_arnparse_lambda():
|
|
274
|
+
result = arnparse("arn:aws:lambda:us-east-1:012345678901:function:ProcessDataHandler")
|
|
275
|
+
assert result.resource_type == "function"
|
|
276
|
+
assert result.resource_id == "ProcessDataHandler"
|
|
277
|
+
assert result.resource_name == "ProcessDataHandler"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_arnparse_logs():
|
|
281
|
+
result = arnparse("arn:aws:logs:us-east-1:012345678901:log-group:/aws/lambda/function-logs")
|
|
282
|
+
assert result.resource_type == "log-group"
|
|
283
|
+
assert result.resource_id == "/aws/lambda/function-logs"
|
|
284
|
+
assert result.resource_name == "/aws/lambda/function-logs"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_arnparse_rds():
|
|
288
|
+
result = arnparse("arn:aws:rds:us-east-1:012345678901:cluster:database-cluster-1")
|
|
289
|
+
assert result.resource_type == "cluster"
|
|
290
|
+
assert result.resource_id == "database-cluster-1"
|
|
291
|
+
|
|
292
|
+
result = arnparse("arn:aws:rds:us-east-1:012345678901:cluster-snapshot:snap001")
|
|
293
|
+
assert result.resource_type == "cluster-snapshot"
|
|
294
|
+
assert result.resource_id == "snap001"
|
|
295
|
+
|
|
296
|
+
result = arnparse("arn:aws:rds:us-east-1:012345678901:db:database-instance-1")
|
|
297
|
+
assert result.resource_type == "db"
|
|
298
|
+
assert result.resource_id == "database-instance-1"
|
|
299
|
+
|
|
300
|
+
result = arnparse("arn:aws:rds:us-east-1:012345678901:ri:reserved01")
|
|
301
|
+
assert result.resource_type == "ri"
|
|
302
|
+
assert result.resource_id == "reserved01"
|
|
303
|
+
|
|
304
|
+
result = arnparse("arn:aws:rds:us-east-1:012345678901:snapshot:final-database-backup-01234567")
|
|
305
|
+
assert result.resource_type == "snapshot"
|
|
306
|
+
assert result.resource_id == "final-database-backup-01234567"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_arnparse_route53():
|
|
310
|
+
result = arnparse("arn:aws:route53:::healthcheck/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
311
|
+
assert result.resource_type == "healthcheck"
|
|
312
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
313
|
+
|
|
314
|
+
result = arnparse("arn:aws:route53:::hostedzone/Z0123456789ABCDEFGHIJ")
|
|
315
|
+
assert result.resource_type == "hostedzone"
|
|
316
|
+
assert result.resource_id == "Z0123456789ABCDEFGHIJ"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_arnparse_s3():
|
|
320
|
+
result = arnparse("arn:aws:s3:::example-bucket-01")
|
|
321
|
+
assert result.resource_type is None
|
|
322
|
+
assert result.resource_id == "example-bucket-01"
|
|
323
|
+
assert result.resource_name == "example-bucket-01"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_arnparse_secretsmanager():
|
|
327
|
+
result = arnparse("arn:aws:secretsmanager:us-east-1:012345678901:secret:/app/secrets/service-name/production-AbCdEf")
|
|
328
|
+
assert result.resource_type == "secret"
|
|
329
|
+
assert result.resource_id == "/app/secrets/service-name/production-AbCdEf"
|
|
330
|
+
assert result.resource_name == "/app/secrets/service-name/production-AbCdEf"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_arnparse_sns():
|
|
334
|
+
result = arnparse("arn:aws:sns:us-east-1:012345678901:topic1")
|
|
335
|
+
assert result.resource_type is None
|
|
336
|
+
assert result.resource_id == "topic1"
|
|
337
|
+
assert result.resource_name == "topic1"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_arnparse_sqs():
|
|
341
|
+
result = arnparse("arn:aws:sqs:us-east-1:012345678901:processing-queue-1")
|
|
342
|
+
assert result.resource_type is None
|
|
343
|
+
assert result.resource_id == "processing-queue-1"
|
|
344
|
+
assert result.resource_name == "processing-queue-1"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_arnparse_ssm():
|
|
348
|
+
result = arnparse("arn:aws:ssm:us-east-1:012345678901:parameter/config_val")
|
|
349
|
+
assert result.resource_type == "parameter"
|
|
350
|
+
assert result.resource_id == "/config_val"
|
|
351
|
+
assert result.resource_name == "/config_val"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_arnparse_wafv2():
|
|
355
|
+
result = arnparse("arn:aws:wafv2:us-east-1:012345678901:global/webacl/WebACL-for-CloudFront-01/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
356
|
+
assert result.resource_type == "global/webacl"
|
|
357
|
+
assert result.resource_id == "WebACL-for-CloudFront-01"
|
|
358
|
+
assert result.resource_name == "WebACL-for-CloudFront-01"
|
|
359
|
+
|
|
360
|
+
result = arnparse("arn:aws:wafv2:us-east-1:012345678901:regional/webacl/webacl-production-acl/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
|
|
361
|
+
assert result.resource_type == "regional/webacl"
|
|
362
|
+
assert result.resource_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
363
|
+
assert result.resource_name == "webacl-production-acl"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"mcp__plugin_adaptivegears_adaptivegears__search",
|
|
5
|
-
"mcp__plugin_adaptivegears_adaptivegears__read",
|
|
6
|
-
"Bash(uv init:*)",
|
|
7
|
-
"Bash(uv venv:*)",
|
|
8
|
-
"Bash(uv add:*)",
|
|
9
|
-
"Bash(make)",
|
|
10
|
-
"Bash(git branch:*)"
|
|
11
|
-
]
|
|
12
|
-
},
|
|
13
|
-
"enabledPlugins": {
|
|
14
|
-
"adaptivegears@adaptivegears-marketplace": true
|
|
15
|
-
}
|
|
16
|
-
}
|
better_aws_tags-0.2.0/PKG-INFO
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: better-aws-tags
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: A unified Python interface for managing AWS resource tags
|
|
5
|
-
Author-email: Andrey Gubarev <andrey@andreygubarev.com>
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Requires-Dist: boto3~=1.0
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# better-aws-tags
|
|
11
|
-
|
|
12
|
-
A unified Python interface for managing AWS resource tags. AWS provides multiple tagging APIs across services, including the Resource Groups Tagging API and service-specific implementations such as EC2, S3, and IAM. This library abstracts these differences behind a single API that accepts any valid ARN and handles service routing, tag validation, and error reporting automatically.
|
|
13
|
-
|
|
14
|
-
## Getting Started
|
|
15
|
-
|
|
16
|
-
```python
|
|
17
|
-
import better_aws_tags as bats
|
|
18
|
-
```
|
better_aws_tags-0.2.0/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# better-aws-tags
|
|
2
|
-
|
|
3
|
-
A unified Python interface for managing AWS resource tags. AWS provides multiple tagging APIs across services, including the Resource Groups Tagging API and service-specific implementations such as EC2, S3, and IAM. This library abstracts these differences behind a single API that accepts any valid ARN and handles service routing, tag validation, and error reporting automatically.
|
|
4
|
-
|
|
5
|
-
## Getting Started
|
|
6
|
-
|
|
7
|
-
```python
|
|
8
|
-
import better_aws_tags as bats
|
|
9
|
-
```
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""AWS resource tag operations."""
|
|
2
|
-
|
|
3
|
-
import boto3
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def get_tags(
|
|
7
|
-
arn: str | list[str],
|
|
8
|
-
session: boto3.Session | None = None,
|
|
9
|
-
) -> dict[str, str] | dict[str, dict[str, str]]:
|
|
10
|
-
"""Get tags for one or more AWS resources.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
arn: Single ARN or list of ARNs.
|
|
14
|
-
session: Optional boto3 session. Uses default if not provided.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Single ARN: {"Key": "Value", ...}
|
|
18
|
-
Multiple ARNs: {"arn1": {"Key": "Value"}, "arn2": {...}}
|
|
19
|
-
"""
|
|
20
|
-
raise NotImplementedError
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def set_tags(
|
|
24
|
-
arn: str | list[str],
|
|
25
|
-
tags: dict[str, str],
|
|
26
|
-
session: boto3.Session | None = None,
|
|
27
|
-
) -> bool | dict[str, bool]:
|
|
28
|
-
"""Set tags on one or more AWS resources.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
arn: Single ARN or list of ARNs.
|
|
32
|
-
tags: Tags to set (merged with existing).
|
|
33
|
-
session: Optional boto3 session. Uses default if not provided.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Single ARN: True if successful.
|
|
37
|
-
Multiple ARNs: {"arn1": True, "arn2": False, ...}
|
|
38
|
-
"""
|
|
39
|
-
raise NotImplementedError
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|