iam-policy-validator 1.8.0__py3-none-any.whl → 1.10.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.
Files changed (49) hide show
  1. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/METADATA +106 -1
  2. iam_policy_validator-1.10.0.dist-info/RECORD +96 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/action_condition_enforcement.py +504 -190
  6. iam_validator/checks/action_resource_matching.py +8 -15
  7. iam_validator/checks/action_validation.py +6 -12
  8. iam_validator/checks/condition_key_validation.py +6 -12
  9. iam_validator/checks/condition_type_mismatch.py +9 -16
  10. iam_validator/checks/full_wildcard.py +9 -13
  11. iam_validator/checks/mfa_condition_check.py +8 -17
  12. iam_validator/checks/policy_size.py +6 -39
  13. iam_validator/checks/policy_structure.py +10 -40
  14. iam_validator/checks/policy_type_validation.py +18 -19
  15. iam_validator/checks/principal_validation.py +11 -20
  16. iam_validator/checks/resource_validation.py +5 -12
  17. iam_validator/checks/sensitive_action.py +8 -15
  18. iam_validator/checks/service_wildcard.py +6 -12
  19. iam_validator/checks/set_operator_validation.py +11 -18
  20. iam_validator/checks/sid_uniqueness.py +8 -38
  21. iam_validator/checks/trust_policy_validation.py +8 -14
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +6 -12
  24. iam_validator/checks/wildcard_resource.py +6 -12
  25. iam_validator/commands/cache.py +4 -3
  26. iam_validator/commands/validate.py +26 -4
  27. iam_validator/core/__init__.py +1 -1
  28. iam_validator/core/aws_fetcher.py +24 -1030
  29. iam_validator/core/aws_service/__init__.py +21 -0
  30. iam_validator/core/aws_service/cache.py +108 -0
  31. iam_validator/core/aws_service/client.py +205 -0
  32. iam_validator/core/aws_service/fetcher.py +612 -0
  33. iam_validator/core/aws_service/parsers.py +149 -0
  34. iam_validator/core/aws_service/patterns.py +51 -0
  35. iam_validator/core/aws_service/storage.py +291 -0
  36. iam_validator/core/aws_service/validators.py +379 -0
  37. iam_validator/core/check_registry.py +82 -14
  38. iam_validator/core/config/defaults.py +10 -0
  39. iam_validator/core/constants.py +17 -0
  40. iam_validator/core/label_manager.py +197 -0
  41. iam_validator/core/policy_checks.py +7 -3
  42. iam_validator/core/pr_commenter.py +34 -7
  43. iam_validator/sdk/__init__.py +1 -1
  44. iam_validator/sdk/context.py +1 -1
  45. iam_validator/sdk/helpers.py +1 -1
  46. iam_policy_validator-1.8.0.dist-info/RECORD +0 -87
  47. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/WHEEL +0 -0
  48. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/entry_points.txt +0 -0
  49. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,612 @@
1
+ """AWS Service Fetcher - Main orchestrator for AWS service data retrieval.
2
+
3
+ This module provides the main AWSServiceFetcher class that coordinates between
4
+ HTTP client, caching, storage, parsing, and validation components.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from iam_validator.core import constants
13
+ from iam_validator.core.aws_service.cache import ServiceCacheManager
14
+ from iam_validator.core.aws_service.client import AWSServiceClient
15
+ from iam_validator.core.aws_service.parsers import ServiceParser
16
+ from iam_validator.core.aws_service.storage import ServiceFileStorage
17
+ from iam_validator.core.aws_service.validators import (
18
+ ConditionKeyValidationResult,
19
+ ServiceValidator,
20
+ )
21
+ from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
22
+ from iam_validator.core.models import ServiceDetail, ServiceInfo
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AWSServiceFetcher:
28
+ """Fetches and validates AWS service information with caching.
29
+
30
+ This is the main entry point for AWS service data operations.
31
+ Coordinates between HTTP client, caching, storage, and validation.
32
+
33
+ Features:
34
+ - Multi-layer caching (memory LRU + disk with TTL)
35
+ - Service pre-fetching for common AWS services
36
+ - Request batching and coalescing
37
+ - Offline mode support with local AWS service files
38
+ - HTTP/2 connection pooling
39
+ - Automatic retry with exponential backoff
40
+
41
+ Example:
42
+ >>> async with AWSServiceFetcher() as fetcher:
43
+ ... # Fetch service list
44
+ ... services = await fetcher.fetch_services()
45
+ ...
46
+ ... # Fetch specific service details
47
+ ... s3_service = await fetcher.fetch_service_by_name("s3")
48
+ ...
49
+ ... # Validate actions
50
+ ... is_valid, error, is_wildcard = await fetcher.validate_action("s3:GetObject")
51
+ """
52
+
53
+ BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
54
+
55
+ # Common AWS services to pre-fetch
56
+ # All other services will be fetched on-demand (lazy loading if found in policies)
57
+ COMMON_SERVICES = [
58
+ "acm",
59
+ "apigateway",
60
+ "autoscaling",
61
+ "backup",
62
+ "batch",
63
+ "bedrock",
64
+ "cloudformation",
65
+ "cloudfront",
66
+ "cloudtrail",
67
+ "cloudwatch",
68
+ "config",
69
+ "dynamodb",
70
+ "ec2-instance-connect",
71
+ "ec2",
72
+ "ecr",
73
+ "ecs",
74
+ "eks",
75
+ "elasticache",
76
+ "elasticloadbalancing",
77
+ "events",
78
+ "firehose",
79
+ "glacier",
80
+ "glue",
81
+ "guardduty",
82
+ "iam",
83
+ "imagebuilder",
84
+ "inspector2",
85
+ "kinesis",
86
+ "kms",
87
+ "lambda",
88
+ "logs",
89
+ "rds",
90
+ "route53",
91
+ "s3",
92
+ "scheduler",
93
+ "secretsmanager",
94
+ "securityhub",
95
+ "sns",
96
+ "sqs",
97
+ "sts",
98
+ "support",
99
+ "waf",
100
+ "wafv2",
101
+ ]
102
+
103
+ def __init__(
104
+ self,
105
+ timeout: float = constants.DEFAULT_HTTP_TIMEOUT_SECONDS,
106
+ retries: int = 3,
107
+ enable_cache: bool = True,
108
+ cache_ttl: int = constants.DEFAULT_CACHE_TTL_SECONDS,
109
+ memory_cache_size: int = 256,
110
+ connection_pool_size: int = 50,
111
+ keepalive_connections: int = 20,
112
+ prefetch_common: bool = True,
113
+ cache_dir: Path | str | None = None,
114
+ aws_services_dir: Path | str | None = None,
115
+ ):
116
+ """Initialize AWS service fetcher.
117
+
118
+ Args:
119
+ timeout: Request timeout in seconds
120
+ retries: Number of retries for failed requests
121
+ enable_cache: Enable persistent disk caching
122
+ cache_ttl: Cache time-to-live in seconds
123
+ memory_cache_size: Size of in-memory LRU cache
124
+ connection_pool_size: HTTP connection pool size
125
+ keepalive_connections: Number of keepalive connections
126
+ prefetch_common: Prefetch common AWS services
127
+ cache_dir: Custom cache directory path
128
+ aws_services_dir: Directory containing pre-downloaded AWS service JSON files.
129
+ When set, the fetcher will load services from local files
130
+ instead of making API calls. Directory should contain:
131
+ - _services.json: List of all services
132
+ - {service}.json: Individual service files (e.g., s3.json)
133
+ """
134
+ self.prefetch_common = prefetch_common
135
+ self.aws_services_dir = Path(aws_services_dir) if aws_services_dir else None
136
+ self._prefetched_services: set[str] = set()
137
+
138
+ # Initialize storage component
139
+ self._storage = ServiceFileStorage(
140
+ cache_dir=cache_dir,
141
+ aws_services_dir=aws_services_dir,
142
+ cache_ttl=cache_ttl,
143
+ enable_cache=enable_cache,
144
+ )
145
+
146
+ # Initialize cache manager
147
+ self._cache = ServiceCacheManager(
148
+ memory_cache_size=memory_cache_size,
149
+ cache_ttl=cache_ttl,
150
+ storage=self._storage if enable_cache else None,
151
+ )
152
+
153
+ # Initialize HTTP client
154
+ self._client = AWSServiceClient(
155
+ base_url=self.BASE_URL,
156
+ timeout=timeout,
157
+ retries=retries,
158
+ connection_pool_size=connection_pool_size,
159
+ keepalive_connections=keepalive_connections,
160
+ )
161
+
162
+ # Initialize parser and validator
163
+ self._parser = ServiceParser()
164
+ self._validator = ServiceValidator(parser=self._parser)
165
+
166
+ async def __aenter__(self) -> "AWSServiceFetcher":
167
+ """Async context manager entry."""
168
+ await self._client.__aenter__()
169
+
170
+ # Pre-fetch common services if enabled
171
+ if self.prefetch_common:
172
+ await self._prefetch_common_services()
173
+
174
+ return self
175
+
176
+ async def __aexit__(
177
+ self,
178
+ exc_type: type[BaseException] | None,
179
+ exc_val: BaseException | None,
180
+ exc_tb: Any,
181
+ ) -> None:
182
+ """Async context manager exit."""
183
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
184
+
185
+ async def _prefetch_common_services(self) -> None:
186
+ """Pre-fetch commonly used AWS services for better performance."""
187
+ logger.info(f"Pre-fetching {len(self.COMMON_SERVICES)} common AWS services...")
188
+
189
+ # First, fetch the services list once to populate the cache
190
+ # This prevents all concurrent calls from fetching the same list
191
+ await self.fetch_services()
192
+
193
+ async def fetch_service(name: str) -> None:
194
+ try:
195
+ await self.fetch_service_by_name(name)
196
+ self._prefetched_services.add(name)
197
+ except Exception as e: # pylint: disable=broad-exception-caught
198
+ logger.warning(f"Failed to prefetch service {name}: {e}")
199
+
200
+ # Fetch in batches to avoid overwhelming the API
201
+ batch_size = 5
202
+ for i in range(0, len(self.COMMON_SERVICES), batch_size):
203
+ batch = self.COMMON_SERVICES[i : i + batch_size]
204
+ await asyncio.gather(*[fetch_service(name) for name in batch])
205
+
206
+ logger.info(f"Pre-fetched {len(self._prefetched_services)} services successfully")
207
+
208
+ async def fetch_services(self) -> list[ServiceInfo]:
209
+ """Fetch list of AWS services with caching.
210
+
211
+ When aws_services_dir is set, loads from local _services.json file.
212
+ Otherwise, fetches from AWS API.
213
+
214
+ Returns:
215
+ List of ServiceInfo objects
216
+
217
+ Example:
218
+ >>> async with AWSServiceFetcher() as fetcher:
219
+ ... services = await fetcher.fetch_services()
220
+ ... print(f"Found {len(services)} AWS services")
221
+ """
222
+ # Check if we have the parsed services list in cache
223
+ services_cache_key = "parsed_services_list"
224
+ cached_services = await self._cache.get(services_cache_key)
225
+ if cached_services is not None and isinstance(cached_services, list):
226
+ logger.debug(f"Retrieved {len(cached_services)} services from parsed cache")
227
+ return cached_services
228
+
229
+ # Load from local file if aws_services_dir is set
230
+ if self.aws_services_dir:
231
+ loaded_services = self._storage.load_services_from_file()
232
+ # Cache the loaded services
233
+ await self._cache.set(services_cache_key, loaded_services)
234
+ return loaded_services
235
+
236
+ # Not in parsed cache, fetch the raw data from API
237
+ data = await self._client.fetch(self.BASE_URL)
238
+
239
+ if not isinstance(data, list):
240
+ raise ValueError("Expected list of services from root endpoint")
241
+
242
+ services: list[ServiceInfo] = []
243
+ for item in data:
244
+ if isinstance(item, dict):
245
+ service = item.get("service")
246
+ url = item.get("url")
247
+ if service and url:
248
+ services.append(ServiceInfo(service=str(service), url=str(url)))
249
+
250
+ # Cache the parsed services list (memory only - raw JSON already cached by client)
251
+ await self._cache.set(services_cache_key, services)
252
+
253
+ # Log only on first fetch (when parsed cache was empty)
254
+ logger.info(f"Fetched and parsed {len(services)} services from AWS API")
255
+ return services
256
+
257
+ async def fetch_service_by_name(self, service_name: str) -> ServiceDetail:
258
+ """Fetch service detail with optimized caching.
259
+
260
+ When aws_services_dir is set, loads from local {service}.json file.
261
+ Otherwise, fetches from AWS API.
262
+
263
+ Args:
264
+ service_name: Name of the service (case-insensitive, e.g., "s3", "iam")
265
+
266
+ Returns:
267
+ ServiceDetail object with full service definition
268
+
269
+ Raises:
270
+ ValueError: If service is not found
271
+
272
+ Example:
273
+ >>> async with AWSServiceFetcher() as fetcher:
274
+ ... s3_service = await fetcher.fetch_service_by_name("s3")
275
+ ... print(f"S3 has {len(s3_service.actions)} actions")
276
+ """
277
+ # Normalize service name
278
+ service_name_lower = service_name.lower()
279
+
280
+ # Check memory cache with service name as key
281
+ cache_key = f"service:{service_name_lower}"
282
+ cached_detail = await self._cache.get(cache_key)
283
+ if isinstance(cached_detail, ServiceDetail):
284
+ logger.debug(f"Memory cache hit for service {service_name}")
285
+ return cached_detail
286
+
287
+ # Load from local file if aws_services_dir is set
288
+ if self.aws_services_dir:
289
+ try:
290
+ service_detail = self._storage.load_service_from_file(service_name_lower)
291
+ # Cache the loaded service
292
+ await self._cache.set(cache_key, service_detail)
293
+ return service_detail
294
+ except FileNotFoundError:
295
+ # Try to find the service in services.json to get proper name
296
+ services = await self.fetch_services()
297
+ for service in services:
298
+ if service.service.lower() == service_name_lower:
299
+ # Try with the exact service name from services.json
300
+ try:
301
+ service_detail = self._storage.load_service_from_file(service.service)
302
+ await self._cache.set(cache_key, service_detail)
303
+ return service_detail
304
+ except FileNotFoundError:
305
+ pass
306
+ raise ValueError(
307
+ f"Service `{service_name}` not found in {self.aws_services_dir}"
308
+ ) from FileNotFoundError
309
+
310
+ # Fetch service list and find URL from API
311
+ services = await self.fetch_services()
312
+
313
+ for service in services:
314
+ if service.service.lower() == service_name_lower:
315
+ # Fetch service detail from API
316
+ data = await self._client.fetch(service.url)
317
+
318
+ # Validate and parse
319
+ service_detail = ServiceDetail.model_validate(data)
320
+
321
+ # Cache with service name as key (memory only - raw JSON already cached by client)
322
+ await self._cache.set(cache_key, service_detail)
323
+
324
+ return service_detail
325
+
326
+ raise ValueError(f"Service `{service_name}` not found")
327
+
328
+ async def fetch_multiple_services(self, service_names: list[str]) -> dict[str, ServiceDetail]:
329
+ """Fetch multiple services concurrently with optimized batching.
330
+
331
+ Args:
332
+ service_names: List of service names to fetch
333
+
334
+ Returns:
335
+ Dictionary mapping service names to ServiceDetail objects
336
+
337
+ Raises:
338
+ Exception: If any service fetch fails
339
+
340
+ Example:
341
+ >>> async with AWSServiceFetcher() as fetcher:
342
+ ... services = await fetcher.fetch_multiple_services(["s3", "iam", "ec2"])
343
+ ... print(f"Fetched {len(services)} services")
344
+ """
345
+
346
+ async def fetch_single(name: str) -> tuple[str, ServiceDetail]:
347
+ try:
348
+ detail = await self.fetch_service_by_name(name)
349
+ return name, detail
350
+ except Exception as e: # pylint: disable=broad-exception-caught
351
+ logger.error(f"Failed to fetch service {name}: {e}")
352
+ raise
353
+
354
+ # Fetch all services concurrently
355
+ tasks = [fetch_single(name) for name in service_names]
356
+ results = await asyncio.gather(*tasks, return_exceptions=True)
357
+
358
+ services: dict[str, ServiceDetail] = {}
359
+ for i, result in enumerate(results):
360
+ if isinstance(result, Exception):
361
+ logger.error(f"Failed to fetch service {service_names[i]}: {result}")
362
+ raise result
363
+ if isinstance(result, tuple):
364
+ name, detail = result
365
+ services[name] = detail
366
+
367
+ return services
368
+
369
+ # --- Validation Methods (delegate to validator) ---
370
+
371
+ async def validate_action(
372
+ self,
373
+ action: str,
374
+ allow_wildcards: bool = True,
375
+ ) -> tuple[bool, str | None, bool]:
376
+ """Validate IAM action.
377
+
378
+ Args:
379
+ action: Full action string (e.g., "s3:GetObject", "iam:CreateUser")
380
+ allow_wildcards: Whether to allow wildcard actions
381
+
382
+ Returns:
383
+ Tuple of (is_valid, error_message, is_wildcard)
384
+
385
+ Example:
386
+ >>> async with AWSServiceFetcher() as fetcher:
387
+ ... is_valid, error, is_wildcard = await fetcher.validate_action("s3:GetObject")
388
+ ... if not is_valid:
389
+ ... print(f"Invalid action: {error}")
390
+ """
391
+ service_prefix, _ = self._parser.parse_action(action)
392
+ service_detail = await self.fetch_service_by_name(service_prefix)
393
+ return await self._validator.validate_action(action, service_detail, allow_wildcards)
394
+
395
+ def validate_arn(self, arn: str) -> tuple[bool, str | None]:
396
+ """Validate ARN format.
397
+
398
+ Args:
399
+ arn: ARN string to validate
400
+
401
+ Returns:
402
+ Tuple of (is_valid, error_message)
403
+
404
+ Example:
405
+ >>> fetcher = AWSServiceFetcher()
406
+ >>> is_valid, error = fetcher.validate_arn("arn:aws:s3:::my-bucket/*")
407
+ >>> if not is_valid:
408
+ ... print(f"Invalid ARN: {error}")
409
+ """
410
+ return self._parser.validate_arn_format(arn)
411
+
412
+ async def validate_condition_key(
413
+ self,
414
+ action: str,
415
+ condition_key: str,
416
+ resources: list[str] | None = None,
417
+ ) -> ConditionKeyValidationResult:
418
+ """Validate condition key.
419
+
420
+ Args:
421
+ action: IAM action (e.g., "s3:GetObject")
422
+ condition_key: Condition key to validate (e.g., "s3:prefix")
423
+ resources: Optional list of resource ARNs
424
+
425
+ Returns:
426
+ ConditionKeyValidationResult with validation details
427
+
428
+ Example:
429
+ >>> async with AWSServiceFetcher() as fetcher:
430
+ ... result = await fetcher.validate_condition_key("s3:GetObject", "s3:prefix")
431
+ ... if not result.is_valid:
432
+ ... print(f"Invalid condition key: {result.error_message}")
433
+ """
434
+ service_prefix, _ = self._parser.parse_action(action)
435
+ service_detail = await self.fetch_service_by_name(service_prefix)
436
+ return await self._validator.validate_condition_key(
437
+ action, condition_key, service_detail, resources
438
+ )
439
+
440
+ # --- Parsing Methods (delegate to parser) ---
441
+
442
+ def parse_action(self, action: str) -> tuple[str, str]:
443
+ """Parse action into service and action name.
444
+
445
+ Args:
446
+ action: Full action string (e.g., "s3:GetObject")
447
+
448
+ Returns:
449
+ Tuple of (service_prefix, action_name)
450
+
451
+ Example:
452
+ >>> fetcher = AWSServiceFetcher()
453
+ >>> service, action_name = fetcher.parse_action("s3:GetObject")
454
+ >>> print(f"Service: {service}, Action: {action_name}")
455
+ """
456
+ return self._parser.parse_action(action)
457
+
458
+ def match_wildcard_action(
459
+ self,
460
+ pattern: str,
461
+ actions: list[str],
462
+ ) -> tuple[bool, list[str]]:
463
+ """Match wildcard pattern against actions.
464
+
465
+ Args:
466
+ pattern: Action pattern with wildcards (e.g., "Get*", "*Object")
467
+ actions: List of action names to match against
468
+
469
+ Returns:
470
+ Tuple of (has_matches, list_of_matched_actions)
471
+
472
+ Example:
473
+ >>> fetcher = AWSServiceFetcher()
474
+ >>> actions = ["GetObject", "PutObject", "DeleteObject"]
475
+ >>> has_matches, matched = fetcher.match_wildcard_action("Get*", actions)
476
+ >>> print(f"Matched: {matched}")
477
+ """
478
+ return self._parser.match_wildcard_action(pattern, actions)
479
+
480
+ # --- Helper Methods ---
481
+
482
+ async def get_resources_for_action(self, action: str) -> list[dict[str, Any]]:
483
+ """Get resource types for action.
484
+
485
+ Args:
486
+ action: Full action name (e.g., "s3:GetObject")
487
+
488
+ Returns:
489
+ List of resource dictionaries from AWS API
490
+
491
+ Example:
492
+ >>> async with AWSServiceFetcher() as fetcher:
493
+ ... resources = await fetcher.get_resources_for_action("s3:GetObject")
494
+ ... print(f"Action operates on {len(resources)} resource types")
495
+ """
496
+ service_prefix, _ = self._parser.parse_action(action)
497
+ service_detail = await self.fetch_service_by_name(service_prefix)
498
+ return self._validator.get_resources_for_action(action, service_detail)
499
+
500
+ async def get_arn_formats_for_action(self, action: str) -> list[str]:
501
+ """Get ARN formats for action.
502
+
503
+ Args:
504
+ action: Full action name (e.g., "s3:GetObject")
505
+
506
+ Returns:
507
+ List of ARN format strings
508
+
509
+ Example:
510
+ >>> async with AWSServiceFetcher() as fetcher:
511
+ ... arns = await fetcher.get_arn_formats_for_action("s3:GetObject")
512
+ ... for arn in arns:
513
+ ... print(f"ARN format: {arn}")
514
+ """
515
+ service_prefix, _ = self._parser.parse_action(action)
516
+ service_detail = await self.fetch_service_by_name(service_prefix)
517
+ return self._validator.get_arn_formats_for_action(action, service_detail)
518
+
519
+ async def get_all_actions_for_service(self, service: str) -> list[str]:
520
+ """Get all actions for service.
521
+
522
+ Args:
523
+ service: Service prefix (e.g., "s3", "iam", "ec2")
524
+
525
+ Returns:
526
+ Sorted list of action names (without service prefix)
527
+
528
+ Example:
529
+ >>> async with AWSServiceFetcher() as fetcher:
530
+ ... actions = await fetcher.get_all_actions_for_service("s3")
531
+ ... print(f"S3 has {len(actions)} actions")
532
+ """
533
+ service_detail = await self.fetch_service_by_name(service)
534
+ return sorted(service_detail.actions.keys())
535
+
536
+ async def expand_wildcard_action(self, action_pattern: str) -> list[str]:
537
+ """Expand wildcard action to full list.
538
+
539
+ Args:
540
+ action_pattern: Action with wildcards (e.g., "iam:Create*", "s3:*Object")
541
+
542
+ Returns:
543
+ Sorted list of fully-qualified actions matching the pattern
544
+
545
+ Example:
546
+ >>> async with AWSServiceFetcher() as fetcher:
547
+ ... actions = await fetcher.expand_wildcard_action("iam:Create*")
548
+ ... print(f"Pattern matches {len(actions)} actions")
549
+ """
550
+ if action_pattern in ("*", "*:*"):
551
+ return ["*"]
552
+
553
+ service_prefix, action_name = self._parser.parse_action(action_pattern)
554
+ service_detail = await self.fetch_service_by_name(service_prefix)
555
+ available = list(service_detail.actions.keys())
556
+ return self._parser.expand_wildcard_to_actions(action_pattern, available, service_prefix)
557
+
558
+ # --- Cache Management ---
559
+
560
+ async def clear_caches(self) -> None:
561
+ """Clear all caches (memory and disk).
562
+
563
+ Example:
564
+ >>> async with AWSServiceFetcher() as fetcher:
565
+ ... await fetcher.clear_caches()
566
+ """
567
+ await self._cache.clear()
568
+
569
+ def set_cache_directory(self, cache_dir: Path | str) -> None:
570
+ """Set a new cache directory path dynamically.
571
+
572
+ This method allows library users to change the cache location at runtime.
573
+ Useful for applications that need to control where cache files are stored.
574
+
575
+ Args:
576
+ cache_dir: New cache directory path
577
+
578
+ Example:
579
+ >>> async with AWSServiceFetcher() as fetcher:
580
+ ... fetcher.set_cache_directory("/tmp/my-custom-cache")
581
+ ... # Future cache operations will use the new directory
582
+ """
583
+ self._storage.set_cache_directory(cache_dir)
584
+
585
+ def get_cache_directory(self) -> Path:
586
+ """Get the current cache directory path.
587
+
588
+ Returns:
589
+ Current cache directory as Path object
590
+
591
+ Example:
592
+ >>> async with AWSServiceFetcher() as fetcher:
593
+ ... cache_path = fetcher.get_cache_directory()
594
+ ... print(f"Cache location: {cache_path}")
595
+ """
596
+ return self._storage.cache_directory
597
+
598
+ def get_stats(self) -> dict[str, Any]:
599
+ """Get fetcher statistics for monitoring.
600
+
601
+ Returns:
602
+ Dictionary with cache and prefetch statistics
603
+
604
+ Example:
605
+ >>> async with AWSServiceFetcher() as fetcher:
606
+ ... stats = fetcher.get_stats()
607
+ ... print(f"Prefetched {stats['prefetched_services']} services")
608
+ """
609
+ return {
610
+ "prefetched_services": len(self._prefetched_services),
611
+ **self._cache.get_stats(),
612
+ }