arnmatch 0.1.0__py3-none-any.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.
- arnmatch/__init__.py +131 -0
- arnmatch/arn_patterns.py +2796 -0
- arnmatch-0.1.0.dist-info/METADATA +6 -0
- arnmatch-0.1.0.dist-info/RECORD +6 -0
- arnmatch-0.1.0.dist-info/WHEEL +4 -0
- arnmatch-0.1.0.dist-info/entry_points.txt +2 -0
arnmatch/__init__.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""ARN pattern matching using regex patterns."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
from .arn_patterns import ARN_PATTERNS
|
|
10
|
+
|
|
11
|
+
# Standard groups that are not resource-specific
|
|
12
|
+
STANDARD_GROUPS = {"Partition", "Region", "Account"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ARNMatchError(ValueError):
|
|
16
|
+
"""Raised when ARN cannot be matched."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ARNMatch:
|
|
23
|
+
"""Result of matching an ARN against patterns."""
|
|
24
|
+
|
|
25
|
+
partition: str
|
|
26
|
+
service: str
|
|
27
|
+
region: str
|
|
28
|
+
account: str
|
|
29
|
+
resource_type: str # canonical type (from AWS docs)
|
|
30
|
+
resource_type_aliases: list[str] # all known names including Resource Explorer
|
|
31
|
+
groups: dict[str, str]
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def resource_id(self) -> str:
|
|
35
|
+
"""Extract resource ID using heuristics.
|
|
36
|
+
|
|
37
|
+
Priority (from end, more specific groups come last):
|
|
38
|
+
1. Group ending with 'Id' (InstanceId, CertificateId, KeyId)
|
|
39
|
+
2. Group ending with 'Name' as fallback
|
|
40
|
+
3. Last non-standard group
|
|
41
|
+
"""
|
|
42
|
+
resource_groups = list(self.groups.items())
|
|
43
|
+
resource_groups = [(k, v) for k, v in resource_groups if k not in STANDARD_GROUPS]
|
|
44
|
+
|
|
45
|
+
# Look for *Id (from end)
|
|
46
|
+
for key, value in reversed(resource_groups):
|
|
47
|
+
if key.endswith("Id"):
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
# Fall back to *Name (from end)
|
|
51
|
+
for key, value in reversed(resource_groups):
|
|
52
|
+
if key.endswith("Name"):
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
# Last resort: last group value
|
|
56
|
+
if resource_groups:
|
|
57
|
+
return resource_groups[-1][1]
|
|
58
|
+
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def resource_name(self) -> str:
|
|
63
|
+
"""Extract resource name using heuristics.
|
|
64
|
+
|
|
65
|
+
Priority (from end, more specific groups come last):
|
|
66
|
+
1. Group ending with 'Name' (FunctionName, BucketName, StackName)
|
|
67
|
+
2. Falls back to resource_id
|
|
68
|
+
"""
|
|
69
|
+
resource_groups = list(self.groups.items())
|
|
70
|
+
resource_groups = [(k, v) for k, v in resource_groups if k not in STANDARD_GROUPS]
|
|
71
|
+
|
|
72
|
+
# Look for *Name (from end)
|
|
73
|
+
for key, value in reversed(resource_groups):
|
|
74
|
+
if key.endswith("Name"):
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
# Fall back to resource_id
|
|
78
|
+
return self.resource_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def arnmatch(arn: str) -> ARNMatch:
|
|
82
|
+
"""Match ARN against patterns.
|
|
83
|
+
|
|
84
|
+
Returns ARNMatch with all captured groups.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ARNMatchError: If ARN cannot be matched.
|
|
88
|
+
"""
|
|
89
|
+
parts = arn.split(":", 5)
|
|
90
|
+
if len(parts) != 6 or parts[0] != "arn":
|
|
91
|
+
raise ARNMatchError(f"Invalid ARN format: {arn}")
|
|
92
|
+
|
|
93
|
+
_, partition, service, region, account, _ = parts
|
|
94
|
+
|
|
95
|
+
if service not in ARN_PATTERNS:
|
|
96
|
+
raise ARNMatchError(f"Unknown service: {service}")
|
|
97
|
+
|
|
98
|
+
for regex, type_names in ARN_PATTERNS[service]:
|
|
99
|
+
match = regex.match(arn)
|
|
100
|
+
if match:
|
|
101
|
+
return ARNMatch(
|
|
102
|
+
partition=partition,
|
|
103
|
+
service=service,
|
|
104
|
+
region=region,
|
|
105
|
+
account=account,
|
|
106
|
+
resource_type=type_names[0], # canonical
|
|
107
|
+
resource_type_aliases=type_names, # all known names
|
|
108
|
+
groups=match.groupdict(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
raise ARNMatchError(f"No pattern matched ARN: {arn}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main() -> None:
|
|
115
|
+
"""CLI entry point."""
|
|
116
|
+
if len(sys.argv) < 2:
|
|
117
|
+
print("Usage: arnmatch <arn>", file=sys.stderr)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
arn = sys.argv[1]
|
|
121
|
+
try:
|
|
122
|
+
result = arnmatch(arn)
|
|
123
|
+
print(f"service: {result.service}")
|
|
124
|
+
print(f"region: {result.region}")
|
|
125
|
+
print(f"account: {result.account}")
|
|
126
|
+
print(f"resource_type: {result.resource_type}")
|
|
127
|
+
print(f"resource_id: {result.resource_id}")
|
|
128
|
+
print(f"resource_name: {result.resource_name}")
|
|
129
|
+
except ARNMatchError as e:
|
|
130
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
131
|
+
sys.exit(1)
|