aws-inventory-manager 0.13.2__py3-none-any.whl → 0.16.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.

Potentially problematic release.


This version of aws-inventory-manager might be problematic. Click here for more details.

@@ -485,7 +485,8 @@ class GroupStore:
485
485
  params.append(region_filter)
486
486
 
487
487
  query = f"""
488
- SELECT r.arn, r.resource_type, r.name, r.canonical_name
488
+ SELECT r.arn, r.resource_type, r.name, r.canonical_name,
489
+ r.normalized_name, r.normalization_method
489
490
  FROM resources r
490
491
  WHERE {" AND ".join(conditions)}
491
492
  ORDER BY r.resource_type, r.name
@@ -494,17 +495,24 @@ class GroupStore:
494
495
  resource_rows = self.db.fetchall(query, tuple(params))
495
496
 
496
497
  # Create group with members
497
- # Use canonical_name (logical ID) when available for stable matching
498
+ # Choose the best match strategy based on how the resource was normalized
498
499
  members = []
499
500
  for row in resource_rows:
500
501
  physical_name = row["name"] or extract_resource_name(row["arn"], row["resource_type"])
501
- canonical_name = row.get("canonical_name")
502
+ normalized_name = row.get("normalized_name")
503
+ normalization_method = row.get("normalization_method") or "none"
502
504
 
503
- # If canonical_name differs from physical_name, it's a CloudFormation logical ID
504
- if canonical_name and canonical_name != physical_name:
505
- resource_name = canonical_name
505
+ # Choose match strategy based on normalization method
506
+ if normalization_method == "tag:logical-id":
507
+ # CloudFormation logical ID - most reliable
508
+ resource_name = row.get("canonical_name") or normalized_name
506
509
  match_strategy = "logical_id"
510
+ elif normalization_method in ("tag:Name", "pattern"):
511
+ # Name tag or pattern extraction - use normalized name
512
+ resource_name = normalized_name or physical_name
513
+ match_strategy = "normalized"
507
514
  else:
515
+ # No normalization - use physical name
508
516
  resource_name = physical_name
509
517
  match_strategy = "physical_name"
510
518
 
@@ -672,10 +680,12 @@ class GroupStore:
672
680
  # Use NOT EXISTS to find resources not in group
673
681
  # Match strategy determines how to compare:
674
682
  # - 'logical_id': match on canonical_name (CloudFormation logical ID)
683
+ # - 'normalized': match on normalized_name (pattern-stripped semantic name)
675
684
  # - 'physical_name': match on physical name or ARN
676
685
  rows = self.db.fetchall(
677
686
  """
678
- SELECT r.arn, r.resource_type, r.name, r.region, r.created_at, r.canonical_name
687
+ SELECT r.arn, r.resource_type, r.name, r.region, r.created_at,
688
+ r.canonical_name, r.normalized_name, r.normalization_method
679
689
  FROM resources r
680
690
  WHERE r.snapshot_id = ?
681
691
  AND NOT EXISTS (
@@ -684,6 +694,7 @@ class GroupStore:
684
694
  AND r.resource_type = gm.resource_type
685
695
  AND (
686
696
  (gm.match_strategy = 'logical_id' AND r.canonical_name = gm.resource_name)
697
+ OR (gm.match_strategy = 'normalized' AND r.normalized_name = gm.resource_name)
687
698
  OR (COALESCE(gm.match_strategy, 'physical_name') = 'physical_name' AND COALESCE(r.name, r.arn) = gm.resource_name)
688
699
  )
689
700
  )
@@ -727,14 +738,17 @@ class GroupStore:
727
738
  # Use INNER JOIN to find resources in group
728
739
  # Match strategy determines how to compare:
729
740
  # - 'logical_id': match on canonical_name (CloudFormation logical ID)
741
+ # - 'normalized': match on normalized_name (pattern-stripped semantic name)
730
742
  # - 'physical_name': match on physical name or ARN
731
743
  rows = self.db.fetchall(
732
744
  """
733
- SELECT r.arn, r.resource_type, r.name, r.region, r.created_at, r.canonical_name
745
+ SELECT r.arn, r.resource_type, r.name, r.region, r.created_at,
746
+ r.canonical_name, r.normalized_name, r.normalization_method
734
747
  FROM resources r
735
748
  INNER JOIN resource_group_members gm
736
749
  ON (
737
750
  (gm.match_strategy = 'logical_id' AND r.canonical_name = gm.resource_name)
751
+ OR (gm.match_strategy = 'normalized' AND r.normalized_name = gm.resource_name)
738
752
  OR (COALESCE(gm.match_strategy, 'physical_name') = 'physical_name' AND COALESCE(r.name, r.arn) = gm.resource_name)
739
753
  )
740
754
  AND r.resource_type = gm.resource_type
@@ -90,6 +90,9 @@ class ResourceStore:
90
90
  r.config_hash,
91
91
  r.created_at,
92
92
  r.source,
93
+ r.canonical_name,
94
+ r.normalized_name,
95
+ r.normalization_method,
93
96
  s.name as snapshot_name,
94
97
  s.created_at as snapshot_created_at,
95
98
  s.account_id
src/storage/schema.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """SQLite schema definitions for AWS Inventory Manager."""
2
2
 
3
- SCHEMA_VERSION = "1.1.0"
3
+ SCHEMA_VERSION = "1.2.0"
4
4
 
5
5
  # Schema creation SQL
6
6
  SCHEMA_SQL = """
@@ -40,6 +40,9 @@ CREATE TABLE IF NOT EXISTS resources (
40
40
  created_at TIMESTAMP,
41
41
  source TEXT DEFAULT 'direct_api',
42
42
  canonical_name TEXT,
43
+ normalized_name TEXT,
44
+ extracted_patterns TEXT,
45
+ normalization_method TEXT,
43
46
  FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE,
44
47
  UNIQUE(snapshot_id, arn)
45
48
  );
@@ -190,6 +193,7 @@ CREATE INDEX IF NOT EXISTS idx_resources_created ON resources(created_at);
190
193
  CREATE INDEX IF NOT EXISTS idx_resources_snapshot ON resources(snapshot_id);
191
194
  CREATE INDEX IF NOT EXISTS idx_resources_type_region ON resources(resource_type, region);
192
195
  CREATE INDEX IF NOT EXISTS idx_resources_canonical_name_type ON resources(canonical_name, resource_type);
196
+ CREATE INDEX IF NOT EXISTS idx_resources_normalized_name_type ON resources(normalized_name, resource_type);
193
197
 
194
198
  -- Tags indexes (for efficient tag queries)
195
199
  CREATE INDEX IF NOT EXISTS idx_tags_resource ON resource_tags(resource_id);
@@ -266,6 +270,53 @@ MIGRATIONS = {
266
270
  WHERE canonical_name IS NULL
267
271
  """,
268
272
  ],
273
+ "1.2.0": [
274
+ # Add normalized_name column for pattern-stripped names
275
+ "ALTER TABLE resources ADD COLUMN normalized_name TEXT",
276
+ # Add extracted_patterns column for storing what was stripped (JSON)
277
+ "ALTER TABLE resources ADD COLUMN extracted_patterns TEXT",
278
+ # Add normalization_method column for tracking how normalization was done
279
+ "ALTER TABLE resources ADD COLUMN normalization_method TEXT",
280
+ # Backfill normalized_name from CloudFormation logical-id tag
281
+ """
282
+ UPDATE resources
283
+ SET normalized_name = (
284
+ SELECT value FROM resource_tags
285
+ WHERE resource_tags.resource_id = resources.id
286
+ AND key = 'aws:cloudformation:logical-id'
287
+ ),
288
+ normalization_method = 'tag:logical-id'
289
+ WHERE normalized_name IS NULL
290
+ AND EXISTS (
291
+ SELECT 1 FROM resource_tags
292
+ WHERE resource_tags.resource_id = resources.id
293
+ AND key = 'aws:cloudformation:logical-id'
294
+ )
295
+ """,
296
+ # Backfill from Name tag
297
+ """
298
+ UPDATE resources
299
+ SET normalized_name = (
300
+ SELECT value FROM resource_tags
301
+ WHERE resource_tags.resource_id = resources.id
302
+ AND key = 'Name'
303
+ ),
304
+ normalization_method = 'tag:Name'
305
+ WHERE normalized_name IS NULL
306
+ AND EXISTS (
307
+ SELECT 1 FROM resource_tags
308
+ WHERE resource_tags.resource_id = resources.id
309
+ AND key = 'Name'
310
+ )
311
+ """,
312
+ # Fallback to physical name (pattern extraction needs Python, done on re-snapshot)
313
+ """
314
+ UPDATE resources
315
+ SET normalized_name = COALESCE(name, arn),
316
+ normalization_method = 'none'
317
+ WHERE normalized_name IS NULL
318
+ """,
319
+ ],
269
320
  }
270
321
 
271
322
 
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
6
6
  from pathlib import Path
7
7
  from typing import Any, Dict, List, Optional
8
8
 
9
+ from ..matching import ResourceNormalizer
9
10
  from ..models.resource import Resource
10
11
  from ..models.snapshot import Snapshot
11
12
  from .database import Database, json_deserialize, json_serialize
@@ -54,6 +55,9 @@ class SnapshotStore:
54
55
  Returns:
55
56
  Database ID of saved snapshot
56
57
  """
58
+ # Create normalizer for computing normalized names
59
+ normalizer = ResourceNormalizer()
60
+
57
61
  with self.db.transaction() as cursor:
58
62
  # Insert snapshot
59
63
  cursor.execute(
@@ -83,13 +87,23 @@ class SnapshotStore:
83
87
 
84
88
  # Insert resources
85
89
  for resource in snapshot.resources:
90
+ # Compute canonical name (for backward compatibility)
86
91
  canonical = compute_canonical_name(resource.name, resource.tags, resource.arn)
92
+
93
+ # Compute normalized name with pattern extraction
94
+ norm_result = normalizer.normalize_single(
95
+ resource.name,
96
+ resource.resource_type,
97
+ resource.tags,
98
+ )
99
+
87
100
  cursor.execute(
88
101
  """
89
102
  INSERT INTO resources (
90
103
  snapshot_id, arn, resource_type, name, region,
91
- config_hash, raw_config, created_at, source, canonical_name
92
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
104
+ config_hash, raw_config, created_at, source, canonical_name,
105
+ normalized_name, extracted_patterns, normalization_method
106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
107
  """,
94
108
  (
95
109
  snapshot_id,
@@ -102,6 +116,9 @@ class SnapshotStore:
102
116
  resource.created_at.isoformat() if resource.created_at else None,
103
117
  resource.source,
104
118
  canonical,
119
+ norm_result.normalized_name,
120
+ json_serialize(norm_result.extracted_patterns),
121
+ norm_result.method,
105
122
  ),
106
123
  )
107
124
  resource_id = cursor.lastrowid
@@ -122,6 +122,9 @@
122
122
  { field: 'resource_type', label: 'Type', visible: true, isBase: true, width: 160, frozen: false },
123
123
  { field: 'region', label: 'Region', visible: true, isBase: true, width: 130, frozen: false },
124
124
  { field: 'snapshot_name', label: 'Snapshot', visible: true, isBase: true, width: 180, frozen: false },
125
+ { field: 'canonical_name', label: 'Canonical Name', visible: false, isBase: true, width: 200, frozen: false },
126
+ { field: 'normalized_name', label: 'Normalized Name', visible: false, isBase: true, width: 200, frozen: false },
127
+ { field: 'normalization_method', label: 'Normalization Method', visible: false, isBase: true, width: 150, frozen: false },
125
128
  { field: 'tags', label: 'All Tags', visible: false, isBase: true, width: 280, frozen: false },
126
129
  { field: 'created_at', label: 'Created', visible: false, isBase: true, width: 180, frozen: false },
127
130
  { field: 'config_hash', label: 'Config Hash', visible: false, isBase: true, width: 160, frozen: false }