diff --git a/c7n/cli.py b/c7n/cli.py index ab32034b5e3..78eb306d523 100644 --- a/c7n/cli.py +++ b/c7n/cli.py @@ -9,6 +9,7 @@ import importlib import logging import os +import pathlib import pdb import sys import traceback @@ -74,10 +75,9 @@ def _default_options(p, exclude=[]): output.add_argument("--debug", default=False, help=argparse.SUPPRESS, action="store_true") - if 'vars' not in exclude: - # p.add_argument('--vars', default=None, - # help='Vars file to substitute into policy') - p.set_defaults(vars=None) + if 'vars-file' not in exclude: + p.add_argument('--vars-file', default=None, type=_valid_path, + help='Vars file to substitute into policy') if 'log-group' not in exclude: p.add_argument( @@ -197,6 +197,17 @@ def _key_val_pair(value): return value +def _valid_path(value): + """ + Type checker to ensure that --vars-file is a path to a file + """ + path = pathlib.Path(value) + if not path.is_file(): + msg = 'vars-file must point to a file' + raise argparse.ArgumentTypeError(msg) + return value + + def setup_parser(): c7n_desc = "Cloud Custodian - Cloud fleet management" parser = argparse.ArgumentParser(description=c7n_desc) diff --git a/c7n/commands.py b/c7n/commands.py index 9a0b937138e..f5b2d9f5437 100644 --- a/c7n/commands.py +++ b/c7n/commands.py @@ -1,6 +1,7 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 from collections import Counter, defaultdict +from collections.abc import Mapping from datetime import timedelta, datetime from functools import wraps import json @@ -50,7 +51,7 @@ def _load_policies(options): options.region = "" for fp in options.configs: try: - collection = policy_load(options, fp, validate=validate, vars=vars) + collection = policy_load(options, fp, validate=validate) except IOError: log.error('policy file does not exist ({})'.format(fp)) errors += 1 @@ -123,7 +124,7 @@ def _load_policies(options): # Variable expansion and non schema validation (not optional) for p in policies: - p.expand_variables(p.get_variables()) + p.expand_variables(p.get_variables(variables=dict(vars or {}))) p.validate() return f(options, list(policies)) @@ -133,11 +134,13 @@ def _load_policies(options): def _load_vars(options): vars = None - if options.vars: + if options.vars_file: try: - vars = load_file(options.vars) - except IOError as e: - log.error('Problem loading vars file "{}": {}'.format(options.vars, e.strerror)) + vars = load_file(options.vars_file) + if not isinstance(vars, Mapping): + raise ValueError("Invalid vars file type") + except (IOError, yaml.YAMLError, ValueError) as e: + log.error('Problem loading vars file "{}": {}'.format(options.vars_file, e)) sys.exit(1) # TODO - provide builtin vars here (such as account) diff --git a/c7n/config.py b/c7n/config.py index bf6873e2ba3..30f433da574 100644 --- a/c7n/config.py +++ b/c7n/config.py @@ -32,6 +32,7 @@ def empty(cls, **kw): 'region': os.environ.get('AWS_DEFAULT_REGION', 'us-east-1'), 'regions': (), 'cache': '', + 'vars_file': None, 'profile': None, 'account_id': None, 'assume_role': None, diff --git a/c7n/element.py b/c7n/element.py index c767359cd7d..1d4e68421a2 100644 --- a/c7n/element.py +++ b/c7n/element.py @@ -2,11 +2,34 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import typing + from c7n import deprecated from c7n.executor import ThreadPoolExecutor from c7n.utils import jmespath_search +class ElementJSONSchema(typing.TypedDict, total=False): + """The shape of a filter/action's ``schema`` class attribute. + + This is a JSON Schema (draft-07) fragment, not a standalone + document: it gets embedded into the combined policy schema (see + ``c7n.schema.process_resource``) at + ``#/definitions/resources//(actions|filters)/``, + and is what validates a single filter/action block in policy + YAML, e.g. ``{'type': 'update', 'rotationPeriod': '...'}``. + + Elements normally build this via the ``type_schema()`` helper + (``c7n.utils.type_schema``) rather than writing it by hand. + """ + + type: str + properties: dict[str, typing.Any] + required: list[str] + additionalProperties: bool + allOf: list[dict[str, typing.Any]] + + class Element: """Parent base class for filters and actions. """ @@ -16,7 +39,8 @@ class Element: executor_factory = ThreadPoolExecutor - schema = {'type': 'object'} + schema: ElementJSONSchema = {'type': 'object'} + # schema aliases get hoisted into a jsonschema definition # location, and then referenced inline. schema_alias = None diff --git a/c7n/resources/apigw.py b/c7n/resources/apigw.py index 1593a0c575a..38f5d2364bc 100644 --- a/c7n/resources/apigw.py +++ b/c7n/resources/apigw.py @@ -1382,3 +1382,46 @@ def process(self, resources): ApiId=r['c7n:parent-id'], StageName=r['StageName'] ) + + +@resources.register("apigwv2-route") +class ApiGatewayV2Route(query.ChildResourceManager): + """Resource manager for API Gateway V2 Routes. + + API Gateway V2 routes define the routing logic for HTTP and WebSocket APIs. + This resource enables filtering routes by authorization type to identify + routes that may lack proper authorization controls. + + :example: + + Find routes with no authorization configured: + + .. code-block:: yaml + + policies: + - name: apigwv2-routes-without-authorization + resource: aws.apigwv2-route + filters: + - type: value + key: AuthorizationType + value: NONE + """ + + class resource_type(query.TypeInfo): + service = "apigatewayv2" + enum_spec = ('get_routes', 'Items', None) + parent_spec = ('aws.apigwv2', 'ApiId', True) + arn_type = "/apis" + id = "RouteId" + name = "RouteKey" + cfn_type = "AWS::ApiGatewayV2::Route" + permission_prefix = 'apigateway' + permissions_enum = ('apigateway:GET',) + + def get_arns(self, resources): + partition = get_partition(self.config.region) + return [ + "arn:{}:apigateway:{}::/apis/{}/routes/{}".format( + partition, self.config.region, r['c7n:parent-id'], r['RouteId'] + ) + for r in resources] diff --git a/c7n/resources/bedrock.py b/c7n/resources/bedrock.py index a9a811cf83f..f31e8572d67 100644 --- a/c7n/resources/bedrock.py +++ b/c7n/resources/bedrock.py @@ -8,11 +8,12 @@ from c7n.manager import resources from c7n.exceptions import PolicyValidationError from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, DescribeWithResourceTags +from c7n.query import RetryPageIterator from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment from c7n.utils import local_session, type_schema, QueryParser from c7n.actions import BaseAction from c7n.filters.kms import KmsRelatedFilter -from c7n.filters import MetricsFilter, ValueFilter +from c7n.filters import Filter, MetricsFilter, ValueFilter from c7n.resources.aws import shape_schema, shape_validate, Arn from c7n.resources.s3 import BucketAssembly, S3_AUGMENT_TABLE @@ -225,6 +226,56 @@ class BedrockCustomModelKmsFilter(KmsRelatedFilter): RelatedIdsExpression = 'modelKmsKeyArn' +@BedrockCustomModel.filter_registry.register('deployments') +class ModelDeploymentsFilter(Filter): + """Filter custom models by their custom model deployments. + + Queries ``ListCustomModelDeployments`` for deployments referencing the + model, optionally scoped server-side by ``status``. Matches + ``value: present`` if any matching deployment is found, or + ``value: absent`` if none is found. + + :example: + + Find custom models with no active deployment: + + .. code-block:: yaml + + policies: + - name: bedrock-custom-model-no-active-deployment + resource: aws.bedrock-custom-model + filters: + - type: deployments + status: Active + value: absent + """ + schema = type_schema( + 'deployments', + status={'enum': ['Creating', 'Active', 'Failed']}, + value={'enum': ['present', 'absent']}, + required=['value']) + permissions = ('bedrock:ListCustomModelDeployments',) + annotation_key = 'c7n:Deployments' + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('bedrock') + params = {} + if 'status' in self.data: + params['statusEquals'] = self.data['status'] + want_present = self.data['value'] == 'present' + results = [] + paginator = client.get_paginator('list_custom_model_deployments') + paginator.PAGE_ITERATOR_CLS = RetryPageIterator + for r in resources: + deployments = paginator.paginate( + modelArnEquals=r['modelArn'], **params + ).build_full_result().get('modelDeploymentSummaries', []) + r[self.annotation_key] = deployments + if bool(deployments) == want_present: + results.append(r) + return results + + class DescribeBedrockCustomizationJob(DescribeSource): def augment(self, resources): diff --git a/c7n/resources/cw.py b/c7n/resources/cw.py index 4bb205aa88f..ac99af30ef2 100644 --- a/c7n/resources/cw.py +++ b/c7n/resources/cw.py @@ -6,7 +6,9 @@ from concurrent.futures import as_completed from datetime import datetime, timedelta -from c7n.actions import BaseAction +from botocore.exceptions import ClientError + +from c7n.actions import AutoTagUser, BaseAction from c7n.exceptions import PolicyValidationError from c7n.filters import Filter, MetricsFilter from c7n.filters.core import parse_date, ValueFilter @@ -17,7 +19,8 @@ QueryResourceManager, TypeInfo, DescribeSource, ConfigSource, DescribeWithResourceTags) from c7n.resolver import ValuesFrom -from c7n.tags import universal_augment +from c7n.tags import ( + RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment) from c7n.utils import type_schema, local_session, chunks, get_retry, jmespath_search @@ -688,6 +691,32 @@ def process(self, resources): logGroupName=r['logGroupName'], **params) +class DescribeDashboard(DescribeSource): + + def augment(self, resources): + # Dashboards aren't supported by the resource groups tagging api, + # fetch their tags from cloudwatch directly. + resources = super().augment(resources) + client = local_session(self.manager.session_factory).client('cloudwatch') + results = [] + for r, arn in zip(resources, self.manager.get_arns(resources)): + try: + tags = self.manager.retry( + client.list_tags_for_resource, ResourceARN=arn).get('Tags', []) + except ClientError as e: + if e.response['Error']['Code'] not in ( + 'ResourceNotFound', 'ResourceNotFoundException'): + raise + # the dashboard was deleted between enumeration and augment + self.manager.log.warning( + "Resource not found: list_tags_for_resource using %s" % { + 'ResourceARN': arn}) + continue + r['Tags'] = tags + results.append(r) + return results + + @resources.register("cloudwatch-dashboard") class CloudWatchDashboard(QueryResourceManager): class resource_type(TypeInfo): @@ -698,14 +727,71 @@ class resource_type(TypeInfo): id = "DashboardName" name = "DashboardName" cfn_type = "AWS::CloudWatch::Dashboard" - universal_taggable = object() global_resource = True + permissions_augment = ("cloudwatch:ListTagsForResource",) source_mapping = { - "describe": DescribeWithResourceTags, + "describe": DescribeDashboard, } +@CloudWatchDashboard.action_registry.register('tag') +class DashboardTag(Tag): + """Add tags to a cloudwatch dashboard + + :example: + + .. code-block:: yaml + + policies: + - name: cloudwatch-dashboard-tag-owner + resource: aws.cloudwatch-dashboard + filters: + - "tag:Owner": absent + actions: + - type: tag + key: Owner + value: platform-team + """ + + permissions = ("cloudwatch:TagResource",) + + def process_resource_set(self, client, resource_set, tags): + for arn in self.manager.get_arns(resource_set): + self.manager.retry(client.tag_resource, ResourceARN=arn, Tags=tags) + + +@CloudWatchDashboard.action_registry.register('remove-tag') +class DashboardRemoveTag(RemoveTag): + """Remove tags from a cloudwatch dashboard + + :example: + + .. code-block:: yaml + + policies: + - name: cloudwatch-dashboard-remove-owner + resource: aws.cloudwatch-dashboard + filters: + - "tag:Owner": present + actions: + - type: remove-tag + tags: [Owner] + """ + + permissions = ("cloudwatch:UntagResource",) + + def process_resource_set(self, client, resource_set, tag_keys): + for arn in self.manager.get_arns(resource_set): + self.manager.retry( + client.untag_resource, ResourceARN=arn, TagKeys=tag_keys) + + +CloudWatchDashboard.filter_registry.register('marked-for-op', TagActionFilter) +CloudWatchDashboard.action_registry.register('mark-for-op', TagDelayedAction) +CloudWatchDashboard.action_registry.register('auto-tag-user', AutoTagUser) + + @resources.register("destination") class Destination(QueryResourceManager): class resource_type(TypeInfo): diff --git a/c7n/resources/ebs.py b/c7n/resources/ebs.py index 0058450d339..087c9508886 100644 --- a/c7n/resources/ebs.py +++ b/c7n/resources/ebs.py @@ -22,7 +22,7 @@ from c7n.resources.kms import ResourceKmsKeyAlias from c7n.resources.securityhub import PostFinding from c7n.query import QueryResourceManager, TypeInfo -from c7n.tags import Tag, coalesce_copy_user_tags +from c7n.tags import coalesce_copy_user_tags from c7n.utils import ( camelResource, chunks, @@ -94,16 +94,6 @@ def get_resources(self, ids, cache=True, augment=True): class ErrorHandler: - @staticmethod - def remove_snapshot(rid, resource_set): - found = None - for r in resource_set: - if r['SnapshotId'] == rid: - found = r - break - if found: - resource_set.remove(found) - @staticmethod def extract_bad_snapshot(e): """Handle various client side errors when describing snapshots""" @@ -132,16 +122,6 @@ def extract_bad_volume(e): log.warning("Volume id malformed %s" % e_vol_id) return e_vol_id - @staticmethod - def remove_volume(rid, resource_set): - found = None - for r in resource_set: - if r['VolumeId'] == rid: - found = r - break - if found: - resource_set.remove(found) - class SnapshotQueryParser(QueryParser): @@ -200,24 +180,6 @@ class VolumeQueryParser(QueryParser): type_name = 'EBS Volume' -@Snapshot.action_registry.register('tag') -class SnapshotTag(Tag): - - permissions = ('ec2:CreateTags',) - - def process_resource_set(self, client, resource_set, tags): - while resource_set: - try: - return super(SnapshotTag, self).process_resource_set( - client, resource_set, tags) - except ClientError as e: - bad_snap = ErrorHandler.extract_bad_snapshot(e) - if bad_snap: - ErrorHandler.remove_snapshot(bad_snap, resource_set) - continue - raise - - @Snapshot.filter_registry.register('age') class SnapshotAge(AgeFilter): """EBS Snapshot Age Filter @@ -739,24 +701,6 @@ def get_resources(self, ids, cache=True, augment=True): return [] -@EBS.action_registry.register('tag') -class VolumeTag(Tag): - - permissions = ('ec2:CreateTags',) - - def process_resource_set(self, client, resource_set, tags): - while resource_set: - try: - return super(VolumeTag, self).process_resource_set( - client, resource_set, tags) - except ClientError as e: - bad_vol = ErrorHandler.extract_bad_volume(e) - if bad_vol: - ErrorHandler.remove_volume(bad_vol, resource_set) - continue - raise - - @EBS.filter_registry.register('snapshots') class EBSSnapshotsFilter(ListItemFilter): """ diff --git a/c7n/resources/eks.py b/c7n/resources/eks.py index 74f0b9d70ad..9c174099c17 100644 --- a/c7n/resources/eks.py +++ b/c7n/resources/eks.py @@ -10,9 +10,11 @@ from c7n.manager import resources from c7n.resources.aws import shape_schema from c7n import tags, query +from c7n.tags import is_resource_gone from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, \ ChildResourceManager, ChildDescribeSource from c7n.utils import local_session, type_schema, get_retry +from botocore.exceptions import ClientError from botocore.waiter import WaiterModel, create_waiter_with_client from .aws import shape_validate from .ecs import ContainerConfigSource @@ -339,7 +341,9 @@ def process_resource_set(self, client, resource_set, tags): client.tag_resource, resourceArn=r['arn'], tags={t['Key']: t['Value'] for t in tags}) - except client.exceptions.ResourceNotFoundException: + except ClientError as e: + if not is_resource_gone(e.response['Error']['Code']): + raise continue @@ -358,7 +362,9 @@ def process_resource_set(self, client, resource_set, tags): self.manager.retry( client.untag_resource, resourceArn=r['arn'], tagKeys=tags) - except client.exceptions.ResourceNotFoundException: + except ClientError as e: + if not is_resource_gone(e.response['Error']['Code']): + raise continue diff --git a/c7n/resources/kms.py b/c7n/resources/kms.py index 50113a5780f..91bedd1305e 100644 --- a/c7n/resources/kms.py +++ b/c7n/resources/kms.py @@ -14,6 +14,7 @@ from c7n.query import ( ConfigSource, DescribeSource, QueryResourceManager, RetryPageIterator, TypeInfo) from c7n.utils import local_session, type_schema, select_keys +from c7n import tags as tagmod from c7n.tags import universal_augment from .securityhub import PostFinding @@ -136,6 +137,46 @@ def alias_map(self): return alias_map +class SkipAwsManagedKey: + """Mixin for kms-key actions that cannot operate on AWS managed keys + (KeyManager == 'AWS', e.g. the keys backing alias/aws/s3, + alias/aws/ebs, etc). AWS owns the lifecycle, policy, and rotation of + these keys; ScheduleKeyDeletion, key policy changes, rotation + changes, and tag mutations are all rejected by the API for them. + Filters them out before delegating to the real action and emits a + warning so operators know why they were skipped.""" + + def process(self, resources): + excluded = [r for r in resources if r.get('KeyManager') == 'AWS'] + if excluded: + self.manager.log.warning( + "Skipping %d AWS managed key(s) which cannot be modified " + "by any customer account", + len(excluded)) + resources = [r for r in resources if r.get('KeyManager') != 'AWS'] + if not resources: + return + return super().process(resources) + + +@Key.action_registry.register('mark') +@Key.action_registry.register('tag') +class KeyTag(SkipAwsManagedKey, tagmod.UniversalTag): + pass + + +@Key.action_registry.register('unmark') +@Key.action_registry.register('untag') +@Key.action_registry.register('remove-tag') +class KeyRemoveTag(SkipAwsManagedKey, tagmod.UniversalUntag): + pass + + +@Key.action_registry.register('mark-for-op') +class KeyMarkForOp(SkipAwsManagedKey, tagmod.UniversalTagDelayedAction): + pass + + @Key.filter_registry.register('key-rotation-status') class KeyRotationStatus(ValueFilter): """Filters KMS keys by the rotation status @@ -313,7 +354,6 @@ def get_matching_aliases(self, resources, event=None): return matched -@Key.action_registry.register('remove-statements') @KeyAlias.action_registry.register('remove-statements') class RemovePolicyStatement(RemovePolicyBase): """Action to remove policy statements from KMS @@ -380,8 +420,12 @@ def process_resource(self, client, resource, key_id): 'Statements': found} -@Key.action_registry.register('set-rotation') -class KmsKeyRotation(BaseAction): +@Key.action_registry.register('remove-statements') +class KeyRemovePolicyStatement(SkipAwsManagedKey, RemovePolicyStatement): + pass + + +class _KmsKeyRotationBase(BaseAction): """Toggle KMS key rotation :example: @@ -411,6 +455,11 @@ def process(self, keys): client.disable_key_rotation(KeyId=k['KeyId']) +@Key.action_registry.register('set-rotation') +class KmsKeyRotation(SkipAwsManagedKey, _KmsKeyRotationBase): + pass + + @KeyAlias.action_registry.register('post-finding') @Key.action_registry.register('post-finding') class KmsPostFinding(PostFinding): @@ -607,8 +656,7 @@ def get_item_values(self, resource): return [result] -@Key.action_registry.register("schedule-deletion") -class KmsKeyScheduleDeletion(BaseAction): +class _KmsKeyScheduleDeletionBase(BaseAction): """Schedule KMS key deletion If the number of days is not specified, the default value of 30 days is used. @@ -641,3 +689,8 @@ def process(self, keys): client.schedule_key_deletion( KeyId=k["KeyId"], PendingWindowInDays=self.data.get("days", 30) ) + + +@Key.action_registry.register("schedule-deletion") +class KmsKeyScheduleDeletion(SkipAwsManagedKey, _KmsKeyScheduleDeletionBase): + pass diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index a9e46514500..a94cfad7e6d 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -11,6 +11,7 @@ "aws.ami": "c7n.resources.ami.AMI", "aws.apigw-domain-name": "c7n.resources.apigw.CustomDomainName", "aws.apigwv2": "c7n.resources.apigw.ApiGwV2", + "aws.apigwv2-route": "c7n.resources.apigw.ApiGatewayV2Route", "aws.apigwv2-stage": "c7n.resources.apigw.ApiGatewayV2Stage", "aws.appmesh-mesh": "c7n.resources.appmesh.AppmeshMesh", "aws.appmesh-virtualgateway": "c7n.resources.appmesh.AppmeshVirtualGateway", diff --git a/c7n/resources/route53.py b/c7n/resources/route53.py index 40eb07d929a..1f37f7b366d 100644 --- a/c7n/resources/route53.py +++ b/c7n/resources/route53.py @@ -616,6 +616,46 @@ class resource_type(TypeInfo): } +class SkipAutoDefinedResolverRule: + """Mixin for ResolverRule actions that cannot operate on Route 53's + global auto-defined rules (e.g. rslvr-autodefined-rr-internet-resolver). + These have no owning account and cannot be tagged or modified by any + customer. Filters them out before delegating to the real action and + emits a warning so operators know why they were skipped.""" + + def process(self, resources): + excluded = [r for r in resources + if r.get('Id', '').startswith('rslvr-autodefined-rr-')] + if excluded: + self.manager.log.warning( + "Skipping %d AWS auto-defined resolver rule(s) which " + "have no owning account and cannot be modified", + len(excluded)) + resources = [r for r in resources + if not r.get('Id', '').startswith('rslvr-autodefined-rr-')] + if not resources: + return + return super().process(resources) + + +@ResolverRule.action_registry.register('mark') +@ResolverRule.action_registry.register('tag') +class ResolverRuleTag(SkipAutoDefinedResolverRule, tags.UniversalTag): + pass + + +@ResolverRule.action_registry.register('unmark') +@ResolverRule.action_registry.register('untag') +@ResolverRule.action_registry.register('remove-tag') +class ResolverRuleRemoveTag(SkipAutoDefinedResolverRule, tags.UniversalUntag): + pass + + +@ResolverRule.action_registry.register('mark-for-op') +class ResolverRuleMarkForOp(SkipAutoDefinedResolverRule, tags.UniversalTagDelayedAction): + pass + + @resources.register('resolver-logs') class ResolverQueryLogConfig(QueryResourceManager): diff --git a/c7n/tags.py b/c7n/tags.py index 443b76f327d..753fb594327 100644 --- a/c7n/tags.py +++ b/c7n/tags.py @@ -8,6 +8,8 @@ snapshots) and resources that support Amazon's Resource Groups Tagging API """ +import re + from collections import Counter from concurrent.futures import as_completed @@ -17,6 +19,8 @@ import time +from botocore.exceptions import ClientError + from c7n.manager import resources as aws_resources from c7n.actions import BaseAction as Action, AutoTagUser from c7n.exceptions import PolicyValidationError, PolicyExecutionError @@ -28,6 +32,54 @@ DEFAULT_TAG = "maid_status" +# Tag actions inherently race with resource deletion, we enumerate a +# resource set and then mutate it. ec2 fails the whole batch when any +# resource in it is unusable, but names the offending ids in the error +# message, so we can drop those and tag the rest. +EC2_BAD_RESOURCE_ID = re.compile(r'^Invalid.+\.(NotFound|Malformed)$') +QUOTED_RESOURCE_IDS = re.compile(r"['\"]([^'\"]+)['\"]") + +# Other services report a resource as already gone with a service specific +# error code - query protocol services use NotFound(Fault), +# json protocol services use (Resource)NotFoundException. +RESOURCE_GONE_ERROR = re.compile(r'NotFound(Fault|Exception)?$') + + +def is_resource_gone(error_code): + """Does an api error code mean the resource we wanted is already gone?""" + return bool(RESOURCE_GONE_ERROR.search(error_code)) + + +def extract_bad_resource_ids(e): + """Extract the resource ids an ec2 api call rejected from its error. + + ec2 names the ids it couldn't use in the error message, ie + ``The instance IDs 'i-0e8f4b9a' do not exist``. + """ + if not EC2_BAD_RESOURCE_ID.match(e.response['Error']['Code']): + return () + return tuple( + rid.strip(" []") + for quoted in QUOTED_RESOURCE_IDS.findall(e.response['Error']['Message']) + for rid in quoted.split(',')) + + +def retry_skip_bad_ids(retry, method, resource_ids, log, **kw): + """Invoke a batched ec2 tag api call, skipping unusable resource ids. + + A resource deleted between enumeration and tagging fails the entire + batch, so drop the ids ec2 rejected and tag the ones still around. + """ + while resource_ids: + try: + return retry(method, Resources=resource_ids, **kw) + except ClientError as e: + bad_ids = set(extract_bad_resource_ids(e)).intersection(resource_ids) + if not bad_ids: + raise + log.info("skipping missing resources %s", ", ".join(sorted(bad_ids))) + resource_ids = [r for r in resource_ids if r not in bad_ids] + # Sentinel: the resolver returns this to omit a tag from a resource's payload. TAG_VALUE_SKIP = Lookup.SKIP @@ -552,9 +604,9 @@ def process(self, resources): def process_resource_set(self, client, resource_set, tags): mid = self.manager.get_model().id - self.manager.retry( - client.create_tags, - Resources=[v[mid] for v in resource_set], + retry_skip_bad_ids( + self.manager.retry, client.create_tags, + [v[mid] for v in resource_set], self.log, Tags=tags, DryRun=self.manager.config.dryrun) @@ -593,9 +645,9 @@ def process(self, resources): self.process_resource_set, self.id_key, resources, tags, self.log) def process_resource_set(self, client, resource_set, tag_keys): - return self.manager.retry( - client.delete_tags, - Resources=[v[self.id_key] for v in resource_set], + return retry_skip_bad_ids( + self.manager.retry, client.delete_tags, + [v[self.id_key] for v in resource_set], self.log, Tags=[{'Key': k} for k in tag_keys], DryRun=self.manager.config.dryrun) @@ -1420,7 +1472,7 @@ def universal_retry(method, ResourceARNList, **kw): error_code = failures[f_arn]['ErrorCode'] if error_code == 'ThrottlingException': throttles.add(f_arn) - elif error_code == 'ResourceNotFoundException': + elif is_resource_gone(error_code): continue else: errors[f_arn] = error_code @@ -1428,6 +1480,10 @@ def universal_retry(method, ResourceARNList, **kw): if errors: raise Exception("Resource Tag Errors %s" % (errors)) + if not throttles: + # everything else was skipped, the api rejects an empty arn list + return response + if idx == max_attempts - 1: raise Exception("Resource Tag Throttled %s" % (", ".join(throttles))) diff --git a/c7n/testing.py b/c7n/testing.py index 65fb22d672d..d16864232aa 100644 --- a/c7n/testing.py +++ b/c7n/testing.py @@ -61,6 +61,19 @@ def write_policy_file(self, policy, format="yaml"): self.addCleanup(fh.close) return fh.name + def write_raw_file(self, contents, format="yaml"): + """Write a raw string to disk. + + Input a string and a format/suffix. + Returns the file path. + """ + fh = tempfile.NamedTemporaryFile(mode="w+b", suffix="." + format, delete=False) + fh.write(contents.encode("utf8")) + fh.flush() + self.addCleanup(os.unlink, fh.name) + self.addCleanup(fh.close) + return fh.name + def get_test_config(self, **kw): temp_dir = self.get_temp_dir() return Config.empty(output_dir=temp_dir, **kw) diff --git a/c7n/utils.py b/c7n/utils.py index 87c071e58f5..cd16c4fa45c 100644 --- a/c7n/utils.py +++ b/c7n/utils.py @@ -14,6 +14,7 @@ import sys import threading import time +import typing from urllib import parse as urlparse from urllib.request import getproxies, proxy_bypass @@ -162,9 +163,17 @@ def parse_date(v, tz=None): return isinstance(v, datetime) and v or None +if typing.TYPE_CHECKING: + from c7n.element import ElementJSONSchema + + def type_schema( - type_name, inherits=None, rinherit=None, - aliases=None, required=None, **props): + type_name: str, + inherits: list[str] | None = None, + rinherit: 'ElementJSONSchema | None' = None, + aliases: list[str] | None = None, + required: list[str] | None = None, + **props: typing.Any) -> 'ElementJSONSchema': """jsonschema generation helper params: diff --git a/c7n/version.py b/c7n/version.py index dea98299b11..9ad6fc746cc 100644 --- a/c7n/version.py +++ b/c7n/version.py @@ -1,2 +1,2 @@ # Generated via tools/dev/devpkg.py -version = "0.9.53a1+e4c099e03" +version = "0.9.53a1+bf0b50dbc" diff --git a/pyproject.toml b/pyproject.toml index c8cea784f48..8eecd5c4e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "urllib3>2", ] name = "c7n" -version = "0.9.53a1+e4c099e03" +version = "0.9.53a1+bf0b50dbc" description = "Cloud Custodian - Policy Rules Engine" readme = "README.md" classifiers = [ diff --git a/tests/common.py b/tests/common.py index 220a2efd92f..3fa93deaeb0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,7 @@ from c7n.testing import TestUtils, TextTestIO, functional # NOQA -from .zpill import PillTest, ACCOUNT_ID +from .zpill import PillTest, ACCOUNT_ID # NOQA logging.getLogger("placebo.pill").setLevel(logging.DEBUG) @@ -33,10 +33,6 @@ class BaseTest(TestUtils, PillTest): # custodian_schema = C7N_SCHEMA - @property - def account_id(self): - return ACCOUNT_ID - def _get_policy_config(self, **kw): if 'account_id' not in kw: kw['account_id'] = self.account_id diff --git a/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetApis_1.json b/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetApis_1.json new file mode 100644 index 00000000000..0083f29d081 --- /dev/null +++ b/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetApis_1.json @@ -0,0 +1,29 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "Items": [ + { + "ApiEndpoint": "https://svr07q9wvj.execute-api.us-east-1.amazonaws.com", + "ApiId": "svr07q9wvj", + "ApiKeySelectionExpression": "$request.header.x-api-key", + "CreatedDate": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 12, + "hour": 23, + "minute": 8, + "second": 1, + "microsecond": 0 + }, + "DisableExecuteApiEndpoint": false, + "IpAddressType": "ipv4", + "Name": "example-api-amazing-kiwi", + "ProtocolType": "HTTP", + "RouteSelectionExpression": "$request.method $request.path", + "Tags": {} + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetRoutes_1.json b/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetRoutes_1.json new file mode 100644 index 00000000000..9445e3083a3 --- /dev/null +++ b/tests/data/placebo/test_apigwv2_route_filter_no_auth/apigateway.GetRoutes_1.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "Items": [ + { + "ApiKeyRequired": false, + "AuthorizationType": "AWS_IAM", + "RouteId": "ck2ui4k", + "RouteKey": "GET /iam-auth", + "Target": "integrations/ipp55qs" + }, + { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "RouteId": "jxtjrzi", + "RouteKey": "GET /no-auth", + "Target": "integrations/ipp55qs" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_apigwv2_route_query/apigateway.GetApis_1.json b/tests/data/placebo/test_apigwv2_route_query/apigateway.GetApis_1.json new file mode 100644 index 00000000000..5ba42059365 --- /dev/null +++ b/tests/data/placebo/test_apigwv2_route_query/apigateway.GetApis_1.json @@ -0,0 +1,29 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "Items": [ + { + "ApiEndpoint": "https://sozi1cborg.execute-api.us-east-1.amazonaws.com", + "ApiId": "sozi1cborg", + "ApiKeySelectionExpression": "$request.header.x-api-key", + "CreatedDate": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 12, + "hour": 23, + "minute": 7, + "second": 48, + "microsecond": 0 + }, + "DisableExecuteApiEndpoint": false, + "IpAddressType": "ipv4", + "Name": "example-api-sharing-duck", + "ProtocolType": "HTTP", + "RouteSelectionExpression": "$request.method $request.path", + "Tags": {} + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_apigwv2_route_query/apigateway.GetRoutes_1.json b/tests/data/placebo/test_apigwv2_route_query/apigateway.GetRoutes_1.json new file mode 100644 index 00000000000..0a945380cdc --- /dev/null +++ b/tests/data/placebo/test_apigwv2_route_query/apigateway.GetRoutes_1.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "Items": [ + { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "RouteId": "ms6wuq0", + "RouteKey": "GET /no-auth", + "Target": "integrations/ev8tgnj" + }, + { + "ApiKeyRequired": false, + "AuthorizationType": "AWS_IAM", + "RouteId": "v7guotm", + "RouteKey": "GET /iam-auth", + "Target": "integrations/ev8tgnj" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.CreateCustomModelDeployment_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.CreateCustomModelDeployment_1.json new file mode 100644 index 00000000000..91507306ccb --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.CreateCustomModelDeployment_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 202, + "data": { + "ResponseMetadata": {}, + "customModelDeploymentArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model-deployment/zboxrwysx8m7" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.DeleteCustomModelDeployment_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.DeleteCustomModelDeployment_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.DeleteCustomModelDeployment_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_1.json new file mode 100644 index 00000000000..51be7bfe10e --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "customModelDeploymentArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model-deployment/zboxrwysx8m7", + "modelDeploymentName": "c7n-test-custom-model-deployment", + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 38, + "second": 56, + "microsecond": 692000 + }, + "status": "Creating", + "lastUpdatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 38, + "second": 56, + "microsecond": 692000 + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_2.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_2.json new file mode 100644 index 00000000000..543148f898e --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModelDeployment_2.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "customModelDeploymentArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model-deployment/zboxrwysx8m7", + "modelDeploymentName": "c7n-test-custom-model-deployment", + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 38, + "second": 56, + "microsecond": 692000 + }, + "status": "Active", + "lastUpdatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 47, + "second": 56, + "microsecond": 535000 + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_1.json new file mode 100644 index 00000000000..8140b45bff9 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_1.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "modelName": "KEEP-c7n-deployable-test-fixture", + "jobArn": "arn:aws:bedrock:us-west-2:644160558196:model-customization-job/meta.llama3-3-70b-instruct-v1:0:128k/tamdglrqxysj", + "baseModelArn": "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k", + "customizationType": "FINE_TUNING", + "hyperParameters": { + "batchSize": "1", + "learningRate": "0.00005", + "epochCount": "1" + }, + "trainingDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-chief-fawn/train.jsonl" + }, + "outputDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-output-chief-fawn/output/" + }, + "trainingMetrics": { + "trainingLoss": -1.0 + }, + "validationMetrics": [], + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 7, + "day": 27, + "hour": 20, + "minute": 22, + "second": 54, + "microsecond": 996000 + }, + "modelStatus": "Active" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_2.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_2.json new file mode 100644 index 00000000000..8140b45bff9 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.GetCustomModel_2.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "modelName": "KEEP-c7n-deployable-test-fixture", + "jobArn": "arn:aws:bedrock:us-west-2:644160558196:model-customization-job/meta.llama3-3-70b-instruct-v1:0:128k/tamdglrqxysj", + "baseModelArn": "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k", + "customizationType": "FINE_TUNING", + "hyperParameters": { + "batchSize": "1", + "learningRate": "0.00005", + "epochCount": "1" + }, + "trainingDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-chief-fawn/train.jsonl" + }, + "outputDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-output-chief-fawn/output/" + }, + "trainingMetrics": { + "trainingLoss": -1.0 + }, + "validationMetrics": [], + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 7, + "day": 27, + "hour": 20, + "minute": 22, + "second": 54, + "microsecond": 996000 + }, + "modelStatus": "Active" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModelDeployments_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModelDeployments_1.json new file mode 100644 index 00000000000..336811fb2a8 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModelDeployments_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelDeploymentSummaries": [ + { + "customModelDeploymentArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model-deployment/zboxrwysx8m7", + "customModelDeploymentName": "c7n-test-custom-model-deployment", + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 38, + "second": 56, + "microsecond": 692000 + }, + "status": "Active", + "lastUpdatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 6, + "hour": 18, + "minute": 47, + "second": 56, + "microsecond": 535000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModels_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModels_1.json new file mode 100644 index 00000000000..da81cd2b3d2 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListCustomModels_1.json @@ -0,0 +1,27 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelSummaries": [ + { + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "modelName": "KEEP-c7n-deployable-test-fixture", + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 7, + "day": 27, + "hour": 20, + "minute": 22, + "second": 54, + "microsecond": 996000 + }, + "baseModelArn": "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k", + "baseModelName": "", + "customizationType": "FINE_TUNING", + "ownerAccountId": "644160558196", + "modelStatus": "Active" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListTagsForResource_1.json b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListTagsForResource_1.json new file mode 100644 index 00000000000..6d623f00255 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_deployments_filter/bedrock.ListTagsForResource_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "owner", + "value": "jim.fulton@sixfeetup.com" + }, + { + "key": "purpose", + "value": "cloud-custodian issue 10843 test fixture" + }, + { + "key": "KEEP", + "value": "jimfulton deployable test custom model" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.GetCustomModel_1.json b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.GetCustomModel_1.json new file mode 100644 index 00000000000..8140b45bff9 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.GetCustomModel_1.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "modelName": "KEEP-c7n-deployable-test-fixture", + "jobArn": "arn:aws:bedrock:us-west-2:644160558196:model-customization-job/meta.llama3-3-70b-instruct-v1:0:128k/tamdglrqxysj", + "baseModelArn": "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k", + "customizationType": "FINE_TUNING", + "hyperParameters": { + "batchSize": "1", + "learningRate": "0.00005", + "epochCount": "1" + }, + "trainingDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-chief-fawn/train.jsonl" + }, + "outputDataConfig": { + "s3Uri": "s3://c7n-bedrock-deployable-output-chief-fawn/output/" + }, + "trainingMetrics": { + "trainingLoss": -1.0 + }, + "validationMetrics": [], + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 7, + "day": 27, + "hour": 20, + "minute": 22, + "second": 54, + "microsecond": 996000 + }, + "modelStatus": "Active" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModelDeployments_1.json b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModelDeployments_1.json new file mode 100644 index 00000000000..947bfafd617 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModelDeployments_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelDeploymentSummaries": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModels_1.json b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModels_1.json new file mode 100644 index 00000000000..da81cd2b3d2 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListCustomModels_1.json @@ -0,0 +1,27 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "modelSummaries": [ + { + "modelArn": "arn:aws:bedrock:us-west-2:644160558196:custom-model/meta.llama3-3-70b-instruct-v1:0:128k/ooyhjc4zw7mw", + "modelName": "KEEP-c7n-deployable-test-fixture", + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 7, + "day": 27, + "hour": 20, + "minute": 22, + "second": 54, + "microsecond": 996000 + }, + "baseModelArn": "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k", + "baseModelName": "", + "customizationType": "FINE_TUNING", + "ownerAccountId": "644160558196", + "modelStatus": "Active" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListTagsForResource_1.json b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListTagsForResource_1.json new file mode 100644 index 00000000000..6d623f00255 --- /dev/null +++ b/tests/data/placebo/test_bedrock_custom_model_undeployed/bedrock.ListTagsForResource_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "owner", + "value": "jim.fulton@sixfeetup.com" + }, + { + "key": "purpose", + "value": "cloud-custodian issue 10843 test fixture" + }, + { + "key": "KEEP", + "value": "jimfulton deployable test custom model" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_1.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_1.json new file mode 100644 index 00000000000..2b5e0228c20 --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "DashboardEntries": [ + { + "DashboardName": "c7n-test-e3dd19005e27e4f1", + "DashboardArn": "arn:aws:cloudwatch::644160558196:dashboard/c7n-test-e3dd19005e27e4f1", + "LastModified": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 8, + "hour": 22, + "minute": 59, + "second": 16, + "microsecond": 0 + }, + "Size": 158 + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_2.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_2.json new file mode 100644 index 00000000000..9280faa7708 --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListDashboards_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "DashboardEntries": [ + { + "DashboardName": "c7n-test-e3dd19005e27e4f1", + "DashboardArn": "arn:aws:cloudwatch::644160558196:dashboard/c7n-test-e3dd19005e27e4f1", + "LastModified": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 8, + "hour": 22, + "minute": 59, + "second": 17, + "microsecond": 0 + }, + "Size": 346 + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_1.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_2.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_2.json new file mode 100644 index 00000000000..2ce4832506c --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_2.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "App", + "Value": "Custodian" + }, + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_3.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_3.json new file mode 100644 index 00000000000..2ce4832506c --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_3.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "App", + "Value": "Custodian" + }, + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_4.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_4.json new file mode 100644 index 00000000000..954290d0fc1 --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.ListTagsForResource_4.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "App", + "Value": "Custodian" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.TagResource_1.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.TagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.TagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.UntagResource_1.json b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.UntagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_cloudwatch_dashboard_tag/monitoring.UntagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_core_network_delete_error/.gitkeep b/tests/data/placebo/test_core_network_delete_error/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/placebo/test_eks_tag_deleted_cluster/eks.TagResource_1.json b/tests/data/placebo/test_eks_tag_deleted_cluster/eks.TagResource_1.json new file mode 100644 index 00000000000..b067a0dbc0b --- /dev/null +++ b/tests/data/placebo/test_eks_tag_deleted_cluster/eks.TagResource_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "Resource was not found", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "Resource was not found" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_eks_tag_deleted_cluster/eks.UntagResource_1.json b/tests/data/placebo/test_eks_tag_deleted_cluster/eks.UntagResource_1.json new file mode 100644 index 00000000000..b067a0dbc0b --- /dev/null +++ b/tests/data/placebo/test_eks_tag_deleted_cluster/eks.UntagResource_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "Resource was not found", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "Resource was not found" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_1.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_1.json new file mode 100644 index 00000000000..ebc15c522c0 --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 400, + "data": { + "Error": { + "Code": "InvalidInstanceID.NotFound", + "Message": "The instance ID 'i-0023456789abcdef0' does not exist" + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_2.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_2.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.CreateTags_2.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DeleteTags_1.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DeleteTags_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DeleteTags_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstanceImageMetadata_1.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstanceImageMetadata_1.json new file mode 100644 index 00000000000..77c70a527a7 --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstanceImageMetadata_1.json @@ -0,0 +1,132 @@ +{ + "status_code": 200, + "data": { + "InstanceImageMetadata": [ + { + "InstanceId": "i-09347f9d6785dbe53", + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 32, + "second": 26, + "microsecond": 0 + }, + "AvailabilityZone": "us-east-1f", + "ZoneId": "use1-az5", + "State": { + "Code": 48, + "Name": "terminated" + }, + "OwnerId": "644160558196", + "ImageMetadata": { + "ImageId": "ami-0757b090c88c85bab", + "Name": "amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "OwnerId": "644160558196", + "State": "available", + "ImageOwnerAlias": "amazon", + "CreationDate": "2026-09-08T17:09:15Z", + "DeprecationTime": "2026-12-07T17:11:00Z", + "IsPublic": true + } + }, + { + "InstanceId": "i-0fd2708167c1cdb5a", + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 32, + "second": 26, + "microsecond": 0 + }, + "AvailabilityZone": "us-east-1f", + "ZoneId": "use1-az5", + "State": { + "Code": 48, + "Name": "terminated" + }, + "OwnerId": "644160558196", + "ImageMetadata": { + "ImageId": "ami-0757b090c88c85bab", + "Name": "amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "OwnerId": "644160558196", + "State": "available", + "ImageOwnerAlias": "amazon", + "CreationDate": "2026-09-08T17:09:15Z", + "DeprecationTime": "2026-12-07T17:11:00Z", + "IsPublic": true + } + }, + { + "InstanceId": "i-0e6f711d31e6675f4", + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AvailabilityZone": "us-east-1c", + "ZoneId": "use1-az2", + "State": { + "Code": 16, + "Name": "running" + }, + "OwnerId": "644160558196", + "ImageMetadata": { + "ImageId": "ami-0757b090c88c85bab", + "Name": "amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "OwnerId": "644160558196", + "State": "available", + "ImageOwnerAlias": "amazon", + "CreationDate": "2026-09-08T17:09:15Z", + "DeprecationTime": "2026-12-07T17:11:00Z", + "IsPublic": true + } + }, + { + "InstanceId": "i-042a559882c4907dc", + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AvailabilityZone": "us-east-1c", + "ZoneId": "use1-az2", + "State": { + "Code": 16, + "Name": "running" + }, + "OwnerId": "644160558196", + "ImageMetadata": { + "ImageId": "ami-0757b090c88c85bab", + "Name": "amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "OwnerId": "644160558196", + "State": "available", + "ImageOwnerAlias": "amazon", + "CreationDate": "2026-09-08T17:09:15Z", + "DeprecationTime": "2026-12-07T17:11:00Z", + "IsPublic": true + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_1.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..737c83c3c3d --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_1.json @@ -0,0 +1,374 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-00cf274ff3ee020d9", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 54, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-0e9414ef1d2eb432c", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-aHfqApEb7WCuXQ32mgMxEZBCP7", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-026ad86d2c3a449f5", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:cd:c8:af:f4:41", + "NetworkInterfaceId": "eni-0816616bd448027b2", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.200", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.200" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "App", + "Value": "Custodian" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-042a559882c4907dc", + "ImageId": "ami-0757b090c88c85bab", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-200.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1c" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "PrivateIpAddress": "10.0.1.200" + } + ] + }, + { + "ReservationId": "r-0bc85187826350a15", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 54, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-039e696c5d3bd6f13", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-f8i4majTXcSBEqpc3kEVYJlrjG", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-074366427f4cb6808", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:68:a3:b5:b6:99", + "NetworkInterfaceId": "eni-086f9944f87e7b575", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.73", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.73" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "App", + "Value": "Custodian" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0e6f711d31e6675f4", + "ImageId": "ami-0757b090c88c85bab", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-73.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1c" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "PrivateIpAddress": "10.0.1.73" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_2.json b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_2.json new file mode 100644 index 00000000000..773b61c1ae8 --- /dev/null +++ b/tests/data/placebo/test_native_tag_skips_gone_instance/ec2.DescribeInstances_2.json @@ -0,0 +1,362 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-00cf274ff3ee020d9", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 54, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-0e9414ef1d2eb432c", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-aHfqApEb7WCuXQ32mgMxEZBCP7", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-026ad86d2c3a449f5", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:cd:c8:af:f4:41", + "NetworkInterfaceId": "eni-0816616bd448027b2", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.200", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.200" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-042a559882c4907dc", + "ImageId": "ami-0757b090c88c85bab", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-200.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1c" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "PrivateIpAddress": "10.0.1.200" + } + ] + }, + { + "ReservationId": "r-0bc85187826350a15", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 54, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-039e696c5d3bd6f13", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-f8i4majTXcSBEqpc3kEVYJlrjG", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-074366427f4cb6808", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:68:a3:b5:b6:99", + "NetworkInterfaceId": "eni-086f9944f87e7b575", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.73", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.73" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-0034c1e69b8e6bcc5", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0e6f711d31e6675f4", + "ImageId": "ami-0757b090c88c85bab", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-73.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 39, + "second": 53, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1c" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-01fd2d48d6f162150", + "VpcId": "vpc-0977a508b64394d7e", + "PrivateIpAddress": "10.0.1.73" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_universal_tag_skips_gone_snapshot/tagging.TagResources_1.json b/tests/data/placebo/test_universal_tag_skips_gone_snapshot/tagging.TagResources_1.json new file mode 100644 index 00000000000..b3fea6b6c33 --- /dev/null +++ b/tests/data/placebo/test_universal_tag_skips_gone_snapshot/tagging.TagResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": { + "arn:aws:elasticache:us-east-1:644160558196:snapshot:c7n-deleted": { + "StatusCode": 404, + "ErrorCode": "SnapshotNotFoundFault", + "ErrorMessage": "c7n-deleted is either not present or not available" + } + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/apigatewayv2_route/main.tf b/tests/terraform/apigatewayv2_route/main.tf new file mode 100644 index 00000000000..c534539d76a --- /dev/null +++ b/tests/terraform/apigatewayv2_route/main.tf @@ -0,0 +1,33 @@ +resource "random_pet" "api" { + prefix = "example-api" +} + +resource "aws_apigatewayv2_api" "example" { + name = random_pet.api.id + protocol_type = "HTTP" + route_selection_expression = "$request.method $request.path" +} + +resource "aws_apigatewayv2_integration" "example" { + api_id = aws_apigatewayv2_api.example.id + integration_type = "HTTP_PROXY" + integration_uri = "https://example.com" + integration_method = "ANY" +} + +# Route with no authorization (should be flagged by policy) +resource "aws_apigatewayv2_route" "no_auth" { + api_id = aws_apigatewayv2_api.example.id + route_key = "GET /no-auth" + target = "integrations/${aws_apigatewayv2_integration.example.id}" + authorization_type = "NONE" +} + +# Route with IAM authorization (should pass policy) +resource "aws_apigatewayv2_route" "iam_auth" { + api_id = aws_apigatewayv2_api.example.id + route_key = "GET /iam-auth" + target = "integrations/${aws_apigatewayv2_integration.example.id}" + authorization_type = "AWS_IAM" +} + diff --git a/tests/terraform/apigatewayv2_route/tf_resources.json b/tests/terraform/apigatewayv2_route/tf_resources.json new file mode 100644 index 00000000000..2c88c1e3723 --- /dev/null +++ b/tests/terraform/apigatewayv2_route/tf_resources.json @@ -0,0 +1,99 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_apigatewayv2_api": { + "example": { + "api_endpoint": "https://svr07q9wvj.execute-api.us-east-1.amazonaws.com", + "api_key_selection_expression": "$request.header.x-api-key", + "arn": "arn:aws:apigateway:us-east-1::/apis/svr07q9wvj", + "body": null, + "cors_configuration": [], + "credentials_arn": null, + "description": "", + "disable_execute_api_endpoint": false, + "execution_arn": "arn:aws:execute-api:us-east-1:644160558196:svr07q9wvj", + "fail_on_warnings": null, + "id": "svr07q9wvj", + "ip_address_type": "ipv4", + "name": "example-api-amazing-kiwi", + "protocol_type": "HTTP", + "region": "us-east-1", + "route_key": null, + "route_selection_expression": "$request.method $request.path", + "tags": null, + "tags_all": {}, + "target": null, + "version": "" + } + }, + "aws_apigatewayv2_integration": { + "example": { + "api_id": "svr07q9wvj", + "connection_id": "", + "connection_type": "INTERNET", + "content_handling_strategy": "", + "credentials_arn": "", + "description": "", + "id": "ipp55qs", + "integration_method": "ANY", + "integration_response_selection_expression": "", + "integration_subtype": "", + "integration_type": "HTTP_PROXY", + "integration_uri": "https://example.com", + "passthrough_behavior": "", + "payload_format_version": "1.0", + "region": "us-east-1", + "request_parameters": null, + "request_templates": null, + "response_parameters": [], + "template_selection_expression": "", + "timeout_milliseconds": 30000, + "tls_config": [] + } + }, + "aws_apigatewayv2_route": { + "iam_auth": { + "api_id": "svr07q9wvj", + "api_key_required": false, + "authorization_scopes": null, + "authorization_type": "AWS_IAM", + "authorizer_id": "", + "id": "ck2ui4k", + "model_selection_expression": "", + "operation_name": "", + "region": "us-east-1", + "request_models": null, + "request_parameter": [], + "route_key": "GET /iam-auth", + "route_response_selection_expression": "", + "target": "integrations/ipp55qs" + }, + "no_auth": { + "api_id": "svr07q9wvj", + "api_key_required": false, + "authorization_scopes": null, + "authorization_type": "NONE", + "authorizer_id": "", + "id": "jxtjrzi", + "model_selection_expression": "", + "operation_name": "", + "region": "us-east-1", + "request_models": null, + "request_parameter": [], + "route_key": "GET /no-auth", + "route_response_selection_expression": "", + "target": "integrations/ipp55qs" + } + }, + "random_pet": { + "api": { + "id": "example-api-amazing-kiwi", + "keepers": null, + "length": 2, + "prefix": "example-api", + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tests/terraform/bedrock_deployable_custom_model/README.md b/tests/terraform/bedrock_deployable_custom_model/README.md new file mode 100644 index 00000000000..379351ba8ab --- /dev/null +++ b/tests/terraform/bedrock_deployable_custom_model/README.md @@ -0,0 +1,139 @@ +# NOTE! + +The fixture set up here is **long-lived**. It takes too long to create to be +created and destroyed when running the tests that need it -- fine-tuning a +custom model takes hours, most of it queued waiting for training capacity. + +So, unlike most fixtures in `tests/terraform`, this one is **applied manually, +out of band**, and is *not* managed by pytest-terraform. No test declares +`@terraform("bedrock_deployable_custom_model")`, and none should. The tests +reference the resulting model only through `DEPLOYABLE_CUSTOM_MODEL_NAME` in +`tests/test_bedrock.py`. + +# Bedrock "deployable" custom model fixture + +Builds the long-lived custom model that backs the `deployments` filter tests on +`aws.bedrock-custom-model` (`test_bedrock_custom_model_deployments_filter`, +`test_bedrock_custom_model_undeployed`). + +## Why the model is not a Terraform resource + +`aws_bedrock_custom_model` *owns* the model -- its destroy deletes the model the +customization job produced. This model must survive `tofu destroy` of its +prerequisites, so Terraform here creates **only** the transient pieces (S3 +buckets, training data object, IAM role) and +`setup.py` submits the customization job. The model is +unmanaged by design: the only way to delete it is a deliberate +`DeleteCustomModel`. + +Related: a customization **job** can never be deleted (there is no +`DeleteModelCustomizationJob` API, only `Stop`), so jobs accumulate in the +account permanently. `setup.py` timestamps each job name to keep them unique. + +`setup.py` also does not wait for the job. Customization takes hours, nearly all +of it queued waiting for training capacity, so blocking is impractical -- and +credentials tend to expire out from under a long wait. It submits and prints the +command to check on it. + +The **deployment** the tests put on this model is not Terraform-managed either, +for a simpler reason: the AWS provider has no custom model deployment resource +(checked against hashicorp/aws 6.56.0). The tests create it through the API, in +a pytest fixture. + +## Why this base model + +`meta.llama3-3-70b-instruct-v1:0:128k` in **us-west-2**, because the +`deployments` filter's populated case needs a custom model whose base model is +eligible for [on-demand custom model +deployment](https://docs.aws.amazon.com/bedrock/latest/userguide/deploy-custom-model-on-demand.html). +Nova models are also eligible (us-east-1) but their fine-tuning jobs do not +complete in this account -- they sit at `trainingDetails: NotStarted` +indefinitely. Llama 3.1 8B trains fine but is *not* deployment-eligible, so it +cannot exercise this filter. + +Note this is text-only fine-tuning despite the docs calling the model "Llama +3.3 70B Vision Instruct": minimum 100 records, Converse API format +(`bedrock-conversation-2024`). `train.jsonl` holds 115 trivial Q&A records; +the content is irrelevant, only a valid completed model is needed. + +## Naming and tags + +The model name is fixed at **`KEEP-c7n-deployable-test-fixture`** -- no random +suffix -- for two reasons: + +1. The tests select it by `modelName`, so a rebuild needs no test-code edit + (only re-recording). The model name never appears in the model ARN, whose + trailing id is AWS-generated and changes on every rebuild. +2. Cleanup in this shared account is a **manual** process, and an earlier build + of this model was deleted by a colleague (confirmed via CloudTrail). Model + names are what a human sees in the console's **Custom models -> Models** tab + and the **Jobs** list. Those columns truncate near 17 characters, so the + warning and the role come first: `KEEP-c7n-deployab...`. + +Tags carry the readable explanation for whoever clicks in (`KEEP`, `owner`, +`purpose`); `setup.py` applies them to both the job and the model. If automated +cleanup is ever introduced, a name prefix will not stop it -- only whatever tag +key the reaper honors, so revisit the `KEEP` key then. + +## Building (or rebuilding) the model + +Requires credentials for the recording account and `tofu` on PATH. + +```bash +cd tests/terraform/bedrock_deployable_custom_model +tofu init +tofu apply # prerequisites only; no model yet +../../../.venv/bin/python setup.py # submits the customization job and exits +``` + +`setup.py` needs `boto3`, so run it with the repo's virtualenv as above +(a bare `./setup.py` will use the system interpreter and fail on the import). + +`setup.py` prints the job ARN and the command to check on it. Expect **hours** +(2.5-3.5h observed end-to-end for this model; data validation alone takes ~3 +minutes, then the job sits queued waiting for training capacity): + +```bash +aws bedrock get-model-customization-job --region us-west-2 \ + --job-identifier \ + --query '{status:status,training:statusDetails.trainingDetails.status,model:outputModelArn}' +``` + +Once `status` is `Completed`, tear down the prerequisites -- the model is not +managed by Terraform and must survive this: + +```bash +tofu destroy +aws bedrock list-custom-models --region us-west-2 # model still listed +``` + +Training cost is trivial (~$0.01 at $0.0033/1K tokens); the model then costs +**$1.95/month** to store for as long as it is kept. + +## The model survives teardown + +Destroying the prerequisites does not affect a completed model: Bedrock holds +its own copy and does not read these buckets at inference time. Verified +2026-07-28 -- after `tofu destroy` the model remained `Active` and could still +be deployed on demand (deployment created, reached `Active`, then deleted). + +To re-check after a future rebuild: + +```bash +aws bedrock get-custom-model --region us-west-2 \ + --model-identifier KEEP-c7n-deployable-test-fixture # Active +aws bedrock create-custom-model-deployment --region us-west-2 \ + --model-deployment-name verify-after-teardown \ + --model-arn # then delete it +``` + +Note a deployment cannot be deleted while it is still `Creating` +(`ValidationException: Custom model deployment is in a mutating state`) -- wait +for `Active` first. + +## After rebuilding + +The model ARN changes (new AWS-generated id), but the **name does not**, so +`DEPLOYABLE_CUSTOM_MODEL_NAME` in `tests/test_bedrock.py` needs no edit. The +recorded flight +data does contain the old ARN, so re-record the affected tests. diff --git a/tests/terraform/bedrock_deployable_custom_model/main.tf b/tests/terraform/bedrock_deployable_custom_model/main.tf new file mode 100644 index 00000000000..22ad2002d04 --- /dev/null +++ b/tests/terraform/bedrock_deployable_custom_model/main.tf @@ -0,0 +1,131 @@ +# Resources needed to create a custom model: buckets, training data, and an +# execution role. Terraform gives us less Python code and easy cleanup once the +# model exists. The model itself isn't defined here, because it needs to +# persist; ./setup.py submits the job that creates it. +# +# Applied by hand, not by a test. See README.md. + +provider "aws" { + region = local.region +} + +locals { + region = "us-west-2" + + # Fixed, not random: tests find the model by name, so a rebuild needs no test + # edit. KEEP- warns off manual cleanup -- see README.md. + custom_model_name = "KEEP-c7n-deployable-test-fixture" + + # Jobs can't be deleted, only stopped, so they pile up -- the username says + # whose they are. setup.py appends a timestamp. + job_name_prefix = "c7n-jimfulton-deployable-test-fixture" + + # Minimal required for fine tuning. + hyperparameters = { + epochCount = "1" + batchSize = "1" + learningRate = "0.00005" + } + + # Not swappable: only a few base models support on-demand deployment, which + # is what the deployments filter needs. See README.md. + base_model_identifier = "arn:aws:bedrock:us-west-2::foundation-model/meta.llama3-3-70b-instruct-v1:0:128k" + + fixture_tags = { + KEEP = "jimfulton deployable test custom model" + owner = "jim.fulton@sixfeetup.com" + purpose = "cloud-custodian issue 10984 test fixture" + } +} + +resource "random_pet" "fixture" { + length = 2 + separator = "-" +} + +resource "aws_s3_bucket" "training" { + bucket = "c7n-bedrock-deployable-${random_pet.fixture.id}" + force_destroy = true +} + +resource "aws_s3_object" "training_data" { + bucket = aws_s3_bucket.training.id + key = "train.jsonl" + source = "${path.module}/train.jsonl" + etag = filemd5("${path.module}/train.jsonl") +} + +resource "aws_s3_bucket" "output" { + bucket = "c7n-bedrock-deployable-output-${random_pet.fixture.id}" + force_destroy = true +} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["bedrock.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "customization" { + name = "c7n-bedrock-deployable-${random_pet.fixture.id}" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "customization_access" { + statement { + actions = ["s3:GetObject", "s3:ListBucket"] + resources = [aws_s3_bucket.training.arn, "${aws_s3_bucket.training.arn}/*"] + } + statement { + actions = ["s3:PutObject", "s3:ListBucket"] + resources = [aws_s3_bucket.output.arn, "${aws_s3_bucket.output.arn}/*"] + } +} + +resource "aws_iam_role_policy" "customization" { + name = "c7n-bedrock-deployable-${random_pet.fixture.id}" + role = aws_iam_role.customization.id + policy = data.aws_iam_policy_document.customization_access.json +} + +# Consumed by ./setup.py via `tofu output -json`. + +output "region" { + value = local.region +} + +output "custom_model_name" { + value = local.custom_model_name +} + +output "job_name_prefix" { + value = local.job_name_prefix +} + +output "hyperparameters" { + value = local.hyperparameters +} + +output "base_model_identifier" { + value = local.base_model_identifier +} + +output "fixture_tags" { + value = local.fixture_tags +} + +output "training_data_s3_uri" { + value = "s3://${aws_s3_bucket.training.id}/${aws_s3_object.training_data.key}" +} + +output "output_s3_uri" { + value = "s3://${aws_s3_bucket.output.id}/output/" +} + +output "execution_role_arn" { + value = aws_iam_role.customization.arn +} diff --git a/tests/terraform/bedrock_deployable_custom_model/setup.py b/tests/terraform/bedrock_deployable_custom_model/setup.py new file mode 100755 index 00000000000..fd36f3919d0 --- /dev/null +++ b/tests/terraform/bedrock_deployable_custom_model/setup.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +"""Submit the customization job that builds this fixture's custom model. + +Doesn't wait for the job: it takes hours. Prints the command to check on it. + +Usage, after `tofu apply` in this directory: + + ../../../.venv/bin/python setup.py +""" + +import json +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +import boto3 +from botocore.exceptions import ClientError + +HERE = Path(__file__).parent +IAM_PROPAGATION_TIMEOUT_SECONDS = 300 +RETRY_SECONDS = 20 + + +def tofu_outputs(): + result = subprocess.run( + ['tofu', 'output', '-json'], + cwd=HERE, check=True, text=True, capture_output=True) + return {k: v['value'] for k, v in json.loads(result.stdout).items()} + + +def as_tag_list(tags): + return [{'key': k, 'value': v} for k, v in sorted(tags.items())] + + +def is_iam_propagation_error(err): + """True for the transient failures seen while a fresh role propagates.""" + code = err.response.get('Error', {}).get('Code', '') + message = err.response.get('Error', {}).get('Message', '').lower() + if code not in ('ValidationException', 'AccessDeniedException'): + return False + return any(token in message for token in ( + 'cannot be assumed', 'unable to assume', 'not authorized', 'role')) + + +def main(): + outputs = tofu_outputs() + region = outputs['region'] + client = boto3.client('bedrock', region_name=region) + + # Job names must be unique. + job_name = '{}-{:%Y%m%d%H%M%S}'.format( + outputs['job_name_prefix'], datetime.now(timezone.utc)) + + request = dict( + jobName=job_name, + customModelName=outputs['custom_model_name'], + roleArn=outputs['execution_role_arn'], + baseModelIdentifier=outputs['base_model_identifier'], + customizationType='FINE_TUNING', + trainingDataConfig={'s3Uri': outputs['training_data_s3_uri']}, + outputDataConfig={'s3Uri': outputs['output_s3_uri']}, + hyperParameters=outputs['hyperparameters'], + jobTags=as_tag_list(outputs['fixture_tags']), + customModelTags=as_tag_list(outputs['fixture_tags']), + ) + + deadline = time.monotonic() + IAM_PROPAGATION_TIMEOUT_SECONDS + while True: + try: + job_arn = client.create_model_customization_job(**request)['jobArn'] + break + except ClientError as err: + if not is_iam_propagation_error(err) or time.monotonic() >= deadline: + raise + print(f'waiting for IAM propagation: {err}') + time.sleep(RETRY_SECONDS) + + print(f'submitted {job_name}') + print(f' {job_arn}') + print() + print('This takes hours. Check on it with:') + print(f' aws bedrock get-model-customization-job --region {region} \\') + print(f' --job-identifier {job_arn} \\') + print(" --query '{status:status," + "training:statusDetails.trainingDetails.status,model:outputModelArn}'") + print() + print('Once status is Completed, tear down these prerequisites with ' + '`tofu destroy`;') + print('the custom model is not managed by Terraform and survives that.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/terraform/bedrock_deployable_custom_model/train.jsonl b/tests/terraform/bedrock_deployable_custom_model/train.jsonl new file mode 100644 index 00000000000..661594c76ff --- /dev/null +++ b/tests/terraform/bedrock_deployable_custom_model/train.jsonl @@ -0,0 +1,115 @@ +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Japan?"}]},{"role":"assistant","content":[{"text":"The capital of Japan is Tokyo."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 15 plus 44?"}]},{"role":"assistant","content":[{"text":"15 plus 44 equals 59."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 27 plus 2?"}]},{"role":"assistant","content":[{"text":"27 plus 2 equals 29."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 50 times 2?"}]},{"role":"assistant","content":[{"text":"50 times 2 equals 100."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 18 times 21?"}]},{"role":"assistant","content":[{"text":"18 times 21 equals 378."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Norway?"}]},{"role":"assistant","content":[{"text":"The capital of Norway is Oslo."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Egypt?"}]},{"role":"assistant","content":[{"text":"The capital of Egypt is Cairo."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Canada?"}]},{"role":"assistant","content":[{"text":"The capital of Canada is Ottawa."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define velocity."}]},{"role":"assistant","content":[{"text":"Velocity is the speed of an object in a given direction."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 39 minus 47?"}]},{"role":"assistant","content":[{"text":"39 minus 47 equals -8."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Italy?"}]},{"role":"assistant","content":[{"text":"The capital of Italy is Rome."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 18 plus 38?"}]},{"role":"assistant","content":[{"text":"18 plus 38 equals 56."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 22 minus 42?"}]},{"role":"assistant","content":[{"text":"22 minus 42 equals -20."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 19 times 44?"}]},{"role":"assistant","content":[{"text":"19 times 44 equals 836."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 28 times 34?"}]},{"role":"assistant","content":[{"text":"28 times 34 equals 952."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Mali?"}]},{"role":"assistant","content":[{"text":"The capital of Mali is Bamako."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 37 plus 8?"}]},{"role":"assistant","content":[{"text":"37 plus 8 equals 45."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 49 times 38?"}]},{"role":"assistant","content":[{"text":"49 times 38 equals 1862."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 21 minus 40?"}]},{"role":"assistant","content":[{"text":"21 minus 40 equals -19."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Spain?"}]},{"role":"assistant","content":[{"text":"The capital of Spain is Madrid."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 26 plus 25?"}]},{"role":"assistant","content":[{"text":"26 plus 25 equals 51."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of France?"}]},{"role":"assistant","content":[{"text":"The capital of France is Paris."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the opposite of up?"}]},{"role":"assistant","content":[{"text":"The opposite of up is down."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 1 times 35?"}]},{"role":"assistant","content":[{"text":"1 times 35 equals 35."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 4 times 48?"}]},{"role":"assistant","content":[{"text":"4 times 48 equals 192."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define democracy."}]},{"role":"assistant","content":[{"text":"Democracy is a system where citizens vote for representatives."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 30 minus 39?"}]},{"role":"assistant","content":[{"text":"30 minus 39 equals -9."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 8 minus 4?"}]},{"role":"assistant","content":[{"text":"8 minus 4 equals 4."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 4 minus 27?"}]},{"role":"assistant","content":[{"text":"4 minus 27 equals -23."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 44 minus 42?"}]},{"role":"assistant","content":[{"text":"44 minus 42 equals 2."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 23 plus 13?"}]},{"role":"assistant","content":[{"text":"23 plus 13 equals 36."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 48 minus 32?"}]},{"role":"assistant","content":[{"text":"48 minus 32 equals 16."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the square root of 64?"}]},{"role":"assistant","content":[{"text":"The square root of 64 is 8."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 30 minus 20?"}]},{"role":"assistant","content":[{"text":"30 minus 20 equals 10."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 33 times 21?"}]},{"role":"assistant","content":[{"text":"33 times 21 equals 693."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 21 plus 21?"}]},{"role":"assistant","content":[{"text":"21 plus 21 equals 42."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 33 times 3?"}]},{"role":"assistant","content":[{"text":"33 times 3 equals 99."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Greece?"}]},{"role":"assistant","content":[{"text":"The capital of Greece is Athens."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 20 minus 34?"}]},{"role":"assistant","content":[{"text":"20 minus 34 equals -14."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What planet do we live on?"}]},{"role":"assistant","content":[{"text":"We live on Earth."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 15 plus 31?"}]},{"role":"assistant","content":[{"text":"15 plus 31 equals 46."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 25 minus 2?"}]},{"role":"assistant","content":[{"text":"25 minus 2 equals 23."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 48 plus 24?"}]},{"role":"assistant","content":[{"text":"48 plus 24 equals 72."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 44 plus 8?"}]},{"role":"assistant","content":[{"text":"44 plus 8 equals 52."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Cuba?"}]},{"role":"assistant","content":[{"text":"The capital of Cuba is Havana."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Thailand?"}]},{"role":"assistant","content":[{"text":"The capital of Thailand is Bangkok."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Poland?"}]},{"role":"assistant","content":[{"text":"The capital of Poland is Warsaw."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 5 minus 20?"}]},{"role":"assistant","content":[{"text":"5 minus 20 equals -15."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 34 minus 31?"}]},{"role":"assistant","content":[{"text":"34 minus 31 equals 3."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Sweden?"}]},{"role":"assistant","content":[{"text":"The capital of Sweden is Stockholm."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 50 times 14?"}]},{"role":"assistant","content":[{"text":"50 times 14 equals 700."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define evaporation."}]},{"role":"assistant","content":[{"text":"Evaporation is the process by which liquid turns into vapor."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Brazil?"}]},{"role":"assistant","content":[{"text":"The capital of Brazil is Brasilia."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Chile?"}]},{"role":"assistant","content":[{"text":"The capital of Chile is Santiago."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Kenya?"}]},{"role":"assistant","content":[{"text":"The capital of Kenya is Nairobi."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 40 times 12?"}]},{"role":"assistant","content":[{"text":"40 times 12 equals 480."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 19 plus 14?"}]},{"role":"assistant","content":[{"text":"19 plus 14 equals 33."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Iraq?"}]},{"role":"assistant","content":[{"text":"The capital of Iraq is Baghdad."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Nepal?"}]},{"role":"assistant","content":[{"text":"The capital of Nepal is Kathmandu."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 1 times 23?"}]},{"role":"assistant","content":[{"text":"1 times 23 equals 23."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 29 minus 47?"}]},{"role":"assistant","content":[{"text":"29 minus 47 equals -18."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 5 plus 10?"}]},{"role":"assistant","content":[{"text":"5 plus 10 equals 15."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 47 minus 40?"}]},{"role":"assistant","content":[{"text":"47 minus 40 equals 7."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define condensation."}]},{"role":"assistant","content":[{"text":"Condensation is the process by which vapor turns into liquid."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define mitosis."}]},{"role":"assistant","content":[{"text":"Mitosis is the division of a cell into two identical cells."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 15 minus 18?"}]},{"role":"assistant","content":[{"text":"15 minus 18 equals -3."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 3 plus 45?"}]},{"role":"assistant","content":[{"text":"3 plus 45 equals 48."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Peru?"}]},{"role":"assistant","content":[{"text":"The capital of Peru is Lima."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 5 minus 1?"}]},{"role":"assistant","content":[{"text":"5 minus 1 equals 4."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 7 times 12?"}]},{"role":"assistant","content":[{"text":"7 times 12 equals 84."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 44 plus 36?"}]},{"role":"assistant","content":[{"text":"44 plus 36 equals 80."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Name a primary color."}]},{"role":"assistant","content":[{"text":"Red is a primary color."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 43 times 20?"}]},{"role":"assistant","content":[{"text":"43 times 20 equals 860."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define inflation."}]},{"role":"assistant","content":[{"text":"Inflation is a general increase in prices over time."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 12 times 42?"}]},{"role":"assistant","content":[{"text":"12 times 42 equals 504."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"How many hours are in a day?"}]},{"role":"assistant","content":[{"text":"There are twenty-four hours in a day."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What color is the sky on a clear day?"}]},{"role":"assistant","content":[{"text":"The sky is blue."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 23 times 38?"}]},{"role":"assistant","content":[{"text":"23 times 38 equals 874."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the fastest land animal?"}]},{"role":"assistant","content":[{"text":"The cheetah is the fastest land animal."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 5 times 5?"}]},{"role":"assistant","content":[{"text":"5 times 5 equals 25."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 50 times 1?"}]},{"role":"assistant","content":[{"text":"50 times 1 equals 50."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 2 times 49?"}]},{"role":"assistant","content":[{"text":"2 times 49 equals 98."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 24 plus 1?"}]},{"role":"assistant","content":[{"text":"24 plus 1 equals 25."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 33 minus 18?"}]},{"role":"assistant","content":[{"text":"33 minus 18 equals 15."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define gravity."}]},{"role":"assistant","content":[{"text":"Gravity is the force that attracts objects toward one another."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is frozen water called?"}]},{"role":"assistant","content":[{"text":"Frozen water is called ice."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 41 plus 47?"}]},{"role":"assistant","content":[{"text":"41 plus 47 equals 88."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 24 times 41?"}]},{"role":"assistant","content":[{"text":"24 times 41 equals 984."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"How many days are in a week?"}]},{"role":"assistant","content":[{"text":"There are seven days in a week."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 25 minus 25?"}]},{"role":"assistant","content":[{"text":"25 minus 25 equals 0."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What gas do plants absorb?"}]},{"role":"assistant","content":[{"text":"Plants absorb carbon dioxide."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define osmosis."}]},{"role":"assistant","content":[{"text":"Osmosis is the movement of water across a semipermeable membrane."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the largest ocean?"}]},{"role":"assistant","content":[{"text":"The Pacific Ocean is the largest."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 48 minus 44?"}]},{"role":"assistant","content":[{"text":"48 minus 44 equals 4."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 8 plus 21?"}]},{"role":"assistant","content":[{"text":"8 plus 21 equals 29."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 8 plus 43?"}]},{"role":"assistant","content":[{"text":"8 plus 43 equals 51."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 13 times 13?"}]},{"role":"assistant","content":[{"text":"13 times 13 equals 169."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define erosion."}]},{"role":"assistant","content":[{"text":"Erosion is the gradual wearing away of rock or soil."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"How many sides does a hexagon have?"}]},{"role":"assistant","content":[{"text":"A hexagon has six sides."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 23 minus 28?"}]},{"role":"assistant","content":[{"text":"23 minus 28 equals -5."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 31 minus 40?"}]},{"role":"assistant","content":[{"text":"31 minus 40 equals -9."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 49 plus 44?"}]},{"role":"assistant","content":[{"text":"49 plus 44 equals 93."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the chemical symbol for water?"}]},{"role":"assistant","content":[{"text":"It is H2O."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"Define photosynthesis."}]},{"role":"assistant","content":[{"text":"Photosynthesis is how plants convert light into chemical energy."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 42 plus 21?"}]},{"role":"assistant","content":[{"text":"42 plus 21 equals 63."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 22 plus 24?"}]},{"role":"assistant","content":[{"text":"22 plus 24 equals 46."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 30 minus 18?"}]},{"role":"assistant","content":[{"text":"30 minus 18 equals 12."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 33 minus 27?"}]},{"role":"assistant","content":[{"text":"33 minus 27 equals 6."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is the capital of Ghana?"}]},{"role":"assistant","content":[{"text":"The capital of Ghana is Accra."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 45 minus 11?"}]},{"role":"assistant","content":[{"text":"45 minus 11 equals 34."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 9 plus 30?"}]},{"role":"assistant","content":[{"text":"9 plus 30 equals 39."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 37 times 11?"}]},{"role":"assistant","content":[{"text":"37 times 11 equals 407."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"How many continents are there?"}]},{"role":"assistant","content":[{"text":"There are seven continents."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"What is 42 times 35?"}]},{"role":"assistant","content":[{"text":"42 times 35 equals 1470."}]}]} +{"schemaVersion":"bedrock-conversation-2024","system":[{"text":"You are a helpful assistant."}],"messages":[{"role":"user","content":[{"text":"How many legs does a spider have?"}]},{"role":"assistant","content":[{"text":"A spider has eight legs."}]}]} diff --git a/tests/terraform/cloudwatch_dashboard_tag/main.tf b/tests/terraform/cloudwatch_dashboard_tag/main.tf new file mode 100644 index 00000000000..6d48460fe8b --- /dev/null +++ b/tests/terraform/cloudwatch_dashboard_tag/main.tf @@ -0,0 +1,26 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +provider "aws" {} + +resource "random_id" "id" { + byte_length = 8 +} + +resource "aws_cloudwatch_dashboard" "test" { + dashboard_name = "c7n-test-${random_id.id.hex}" + + dashboard_body = jsonencode({ + widgets = [ + { + type = "text" + x = 0 + y = 0 + width = 6 + height = 6 + properties = { + markdown = "cloud custodian test dashboard" + } + } + ] + }) +} diff --git a/tests/terraform/cloudwatch_dashboard_tag/tf_resources.json b/tests/terraform/cloudwatch_dashboard_tag/tf_resources.json new file mode 100644 index 00000000000..61561c8fbc4 --- /dev/null +++ b/tests/terraform/cloudwatch_dashboard_tag/tf_resources.json @@ -0,0 +1,27 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_cloudwatch_dashboard": { + "test": { + "dashboard_arn": "arn:aws:cloudwatch::644160558196:dashboard/c7n-test-e3dd19005e27e4f1", + "dashboard_body": "{\"widgets\":[{\"height\":6,\"properties\":{\"markdown\":\"cloud custodian test dashboard\"},\"type\":\"text\",\"width\":6,\"x\":0,\"y\":0}]}", + "dashboard_name": "c7n-test-e3dd19005e27e4f1", + "id": "c7n-test-e3dd19005e27e4f1", + "region": "us-east-2" + } + }, + "random_id": { + "id": { + "b64_std": "490ZAF4n5PE=", + "b64_url": "490ZAF4n5PE", + "byte_length": 8, + "dec": "16419307305833063665", + "hex": "e3dd19005e27e4f1", + "id": "490ZAF4n5PE", + "keepers": null, + "prefix": null + } + } + } +} \ No newline at end of file diff --git a/tests/terraform/ec2_instance_tag/main.tf b/tests/terraform/ec2_instance_tag/main.tf new file mode 100644 index 00000000000..61796ec1aac --- /dev/null +++ b/tests/terraform/ec2_instance_tag/main.tf @@ -0,0 +1,36 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +provider "aws" {} + +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } +} + +resource "aws_vpc" "example" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "example" { + vpc_id = aws_vpc.example.id + cidr_block = "10.0.1.0/24" +} + +# two instances, so a batch that loses one id still has more than one +# survivor to prove the retry keeps all of them +resource "aws_instance" "first" { + ami = data.aws_ami.amazon_linux.id + instance_type = "t2.micro" + subnet_id = aws_subnet.example.id +} + +resource "aws_instance" "second" { + ami = data.aws_ami.amazon_linux.id + instance_type = "t2.micro" + subnet_id = aws_subnet.example.id +} diff --git a/tests/terraform/ec2_instance_tag/tf_resources.json b/tests/terraform/ec2_instance_tag/tf_resources.json new file mode 100644 index 00000000000..06b1ea31101 --- /dev/null +++ b/tests/terraform/ec2_instance_tag/tf_resources.json @@ -0,0 +1,392 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_ami": { + "amazon_linux": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-1::image/ami-0757b090c88c85bab", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "0", + "snapshot_id": "snap-0b2af24c3d555bc22", + "throughput": "0", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp2" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "", + "creation_date": "2026-09-08T17:09:15.000Z", + "deprecation_time": "2026-12-07T17:11:00.000Z", + "description": "Amazon Linux 2 AMI 2.0.20260908.0 x86_64 HVM gp2", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "name", + "values": [ + "amzn2-ami-hvm-*-x86_64-gp2" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0757b090c88c85bab", + "image_id": "ami-0757b090c88c85bab", + "image_location": "amazon/amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": true, + "name": "amzn2-ami-hvm-2.0.20260908.0-x86_64-gp2", + "name_regex": null, + "owner_id": "644160558196", + "owners": [ + "amazon" + ], + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-1", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-0b2af24c3d555bc22", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + } + }, + "aws_instance": { + "first": { + "ami": "ami-0757b090c88c85bab", + "arn": "arn:aws:ec2:us-east-1:644160558196:instance/i-0e6f711d31e6675f4", + "associate_public_ip_address": false, + "availability_zone": "us-east-1c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0e6f711d31e6675f4", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-086f9944f87e7b575" + } + ], + "primary_network_interface_id": "eni-086f9944f87e7b575", + "private_dns": "ip-10-0-1-73.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.1.73", + "public_dns": "", + "public_ip": "", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": true, + "iops": 100, + "kms_key_id": "arn:aws:kms:us-east-1:644160558196:key/04fa09fb-afe3-46c8-ad3c-410b8047f7b9", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-039e696c5d3bd6f13", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-01fd2d48d6f162150", + "tags": null, + "tags_all": {}, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0034c1e69b8e6bcc5" + ] + }, + "second": { + "ami": "ami-0757b090c88c85bab", + "arn": "arn:aws:ec2:us-east-1:644160558196:instance/i-042a559882c4907dc", + "associate_public_ip_address": false, + "availability_zone": "us-east-1c", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-042a559882c4907dc", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0816616bd448027b2" + } + ], + "primary_network_interface_id": "eni-0816616bd448027b2", + "private_dns": "ip-10-0-1-200.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.1.200", + "public_dns": "", + "public_ip": "", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": true, + "iops": 100, + "kms_key_id": "arn:aws:kms:us-east-1:644160558196:key/04fa09fb-afe3-46c8-ad3c-410b8047f7b9", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-0e9414ef1d2eb432c", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-01fd2d48d6f162150", + "tags": null, + "tags_all": {}, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-0034c1e69b8e6bcc5" + ] + } + }, + "aws_subnet": { + "example": { + "arn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-01fd2d48d6f162150", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-1c", + "availability_zone_id": "use1-az2", + "cidr_block": "10.0.1.0/24", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-01fd2d48d6f162150", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "644160558196", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0977a508b64394d7e" + } + }, + "aws_vpc": { + "example": { + "arn": "arn:aws:ec2:us-east-1:644160558196:vpc/vpc-0977a508b64394d7e", + "assign_generated_ipv6_cidr_block": false, + "cidr_block": "10.0.0.0/16", + "default_network_acl_id": "acl-05bc7b1ba43105f96", + "default_route_table_id": "rtb-02919e3e487a62814", + "default_security_group_id": "sg-0034c1e69b8e6bcc5", + "dhcp_options_id": "dopt-05763bf1939671093", + "enable_dns_hostnames": false, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "id": "vpc-0977a508b64394d7e", + "instance_tenancy": "default", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_network_border_group": "", + "ipv6_ipam_pool_id": "", + "ipv6_netmask_length": 0, + "main_route_table_id": "rtb-02919e3e487a62814", + "owner_id": "644160558196", + "region": "us-east-1", + "tags": null, + "tags_all": {} + } + } + } +} \ No newline at end of file diff --git a/tests/test_apigw.py b/tests/test_apigw.py index ed4a3a43610..6e781290916 100644 --- a/tests/test_apigw.py +++ b/tests/test_apigw.py @@ -32,6 +32,52 @@ def test_apigwv2_stage_query(test, apigatewayv2_stage): ] +@terraform("apigatewayv2_route") +def test_apigwv2_route_query(test, apigatewayv2_route): + factory = test.replay_flight_data("test_apigwv2_route_query") + + policy = test.load_policy({ + "name": "test-aws-apigwv2-route", + "resource": "aws.apigwv2-route" + }, session_factory=factory) + + resources = policy.run() + + assert len(resources) > 0 + assert 'RouteId' in resources[0] + assert 'RouteKey' in resources[0] + assert 'AuthorizationType' in resources[0] + assert 'c7n:parent-id' in resources[0] + + assert policy.resource_manager.get_arns(resources) == [ + 'arn:aws:apigateway:us-east-1::/apis/sozi1cborg/routes/ms6wuq0', + 'arn:aws:apigateway:us-east-1::/apis/sozi1cborg/routes/v7guotm' + ] + + +@terraform("apigatewayv2_route") +def test_apigwv2_route_filter_no_auth(test, apigatewayv2_route): + factory = test.replay_flight_data("test_apigwv2_route_filter_no_auth") + + policy = test.load_policy({ + "name": "test-aws-apigwv2-route-no-auth", + "resource": "aws.apigwv2-route", + "filters": [ + { + "type": "value", + "key": "AuthorizationType", + "value": "NONE" + } + ] + }, session_factory=factory) + + resources = policy.run() + + assert len(resources) >= 1 + for r in resources: + assert r['AuthorizationType'] == 'NONE' + + class TestRestAccount(BaseTest): def test_missing_rest_account(self): diff --git a/tests/test_bedrock.py b/tests/test_bedrock.py index 3ad66f9d4de..4fc061966da 100644 --- a/tests/test_bedrock.py +++ b/tests/test_bedrock.py @@ -1,10 +1,10 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 import logging -from unittest import mock - +import time import pytest +from unittest import mock from .common import ACCOUNT_ID, BaseTest, event_data from botocore.exceptions import ClientError from pytest_terraform import terraform @@ -433,6 +433,20 @@ def test_bedrock_custom_model(self): self.assertEqual(len(tags), 1) self.assertEqual(tags, [{'key': 'foo', 'value': 'bar'}]) + def test_bedrock_custom_model_deployments_filter_schema(self): + session_factory = self.replay_flight_data('test_bedrock_custom_model') + p = self.load_policy( + { + 'name': 'bedrock-custom-model-no-active-deployment', + 'resource': 'bedrock-custom-model', + 'filters': [ + {'type': 'deployments', 'status': 'Active', 'value': 'absent'}, + ], + }, + session_factory=session_factory, + ) + self.assertTrue(p) + def test_bedrock_custom_model_delete(self): session_factory = self.replay_flight_data('test_bedrock_custom_model_delete') p = self.load_policy( @@ -1711,6 +1725,111 @@ def test_bedrock_inference_profile_bad_statistics(test): ) +# A long-lived model, not built per test run -- fine-tuning takes hours. Found +# by name, since a rebuild changes its ARN. +# See tests/terraform/bedrock_deployable_custom_model. +DEPLOYABLE_CUSTOM_MODEL_NAME = "KEEP-c7n-deployable-test-fixture" +DEPLOYABLE_CUSTOM_MODEL_REGION = "us-west-2" + + +def wait_for_custom_model_deployment_active(client, deployment_arn, test): + status = None + for _ in range(60): + status = client.get_custom_model_deployment( + customModelDeploymentIdentifier=deployment_arn)['status'] + if status == 'Active': + return + if status == 'Failed': + raise RuntimeError(f'custom model deployment {deployment_arn} failed') + if test.recording: + time.sleep(20) + raise RuntimeError( + f'custom model deployment {deployment_arn} did not become Active: {status}') + + +@pytest.fixture +def create_custom_model_deployment(test): + """Create an on-demand custom model deployment, and clean it up after. + + Not a Terraform fixture: the AWS provider has no deployment resource. + + Yields ``(model_arn, region) -> deployment_arn``. Set + ``test.session_factory`` first, so its calls record with the test. + """ + created = [] + + def _create(model_arn, region, name="c7n-test-custom-model-deployment"): + client = test.session_factory().client('bedrock', region_name=region) + deployment_arn = client.create_custom_model_deployment( + modelDeploymentName=name, modelArn=model_arn)['customModelDeploymentArn'] + created.append((client, deployment_arn)) + wait_for_custom_model_deployment_active(client, deployment_arn, test) + return deployment_arn + + try: + yield _create + finally: + for client, deployment_arn in created: + try: + client.delete_custom_model_deployment( + customModelDeploymentIdentifier=deployment_arn) + except client.exceptions.ResourceNotFoundException: + pass + + +def test_bedrock_custom_model_deployments_filter(test, create_custom_model_deployment): + test.session_factory = test.replay_flight_data( + 'test_bedrock_custom_model_deployments_filter', + region=DEPLOYABLE_CUSTOM_MODEL_REGION) + + # CreateCustomModelDeployment needs the ARN; GetCustomModel takes the name. + client = test.session_factory().client( + 'bedrock', region_name=DEPLOYABLE_CUSTOM_MODEL_REGION) + model_arn = client.get_custom_model( + modelIdentifier=DEPLOYABLE_CUSTOM_MODEL_NAME)['modelArn'] + + create_custom_model_deployment(model_arn, DEPLOYABLE_CUSTOM_MODEL_REGION) + + # Unscoped, as a real policy would be. + present = test.load_policy( + { + 'name': 'bedrock-custom-model-active-deployment', + 'resource': 'aws.bedrock-custom-model', + 'filters': [ + {'type': 'deployments', 'status': 'Active', 'value': 'present'}, + ], + }, + session_factory=test.session_factory, + config={'region': DEPLOYABLE_CUSTOM_MODEL_REGION}, + ) + resources = present.run() + + # Shared account, so other models may come and go -- only assert ours. + assert any(r['modelName'] == DEPLOYABLE_CUSTOM_MODEL_NAME for r in resources) + + +def test_bedrock_custom_model_undeployed(test): + # An Active model with no deployment: costs storage while serving nothing. + test.session_factory = test.replay_flight_data( + 'test_bedrock_custom_model_undeployed', + region=DEPLOYABLE_CUSTOM_MODEL_REGION) + + policy = test.load_policy( + { + 'name': 'bedrock-custom-model-undeployed', + 'resource': 'aws.bedrock-custom-model', + 'filters': [ + {'type': 'deployments', 'status': 'Active', 'value': 'absent'}, + ], + }, + session_factory=test.session_factory, + config={'region': DEPLOYABLE_CUSTOM_MODEL_REGION}, + ) + resources = policy.run() + + assert any(r['modelName'] == DEPLOYABLE_CUSTOM_MODEL_NAME for r in resources) + + class BedrockMantleProject(BaseTest): def test_bedrock_mantle_project_query(self): diff --git a/tests/test_cli.py b/tests/test_cli.py index 0509c2715d5..91405ab793d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import json import os +import pathlib import sys import argparse @@ -86,6 +87,16 @@ def test_key_val_pair(self): param = "day=today" self.assertIs(cli._key_val_pair(param), param) + def test_valid_path(self): + self.assertRaises(ArgumentTypeError, cli._valid_path, "invalid option") + param = "mock_path" + self.patch(pathlib.Path, "is_file", lambda x: True) + self.assertIs(cli._valid_path(param), param) + self.patch(pathlib.Path, "is_file", lambda x: False) + with self.assertRaises(ArgumentTypeError) as context: + cli._valid_path(param) + self.assertEqual(str(context.exception), 'vars-file must point to a file') + class VersionTest(CliTest): @@ -538,6 +549,97 @@ def test_ec2(self): ] ) + def test_vars_file(self): + session_factory = self.replay_flight_data( + "test_ec2_state_transition_age_filter" + ) + + from c7n.policy import PolicyCollection + + self.patch( + PolicyCollection, + "session_factory", + staticmethod(lambda x=None: session_factory), + ) + + temp_dir = self.get_temp_dir() + policy_name = "ec2-vars-file" + yaml_file = self.write_policy_file( + { + "policies": [ + { + "name": policy_name, + "resource": "ec2", + "filters": [{"State.Name": "{injected_from_vars}"}], + } + ] + } + ) + vars_file = self.write_policy_file({"injected_from_vars": "running"}, format="json") + + self.run_and_expect_success( + [ + "custodian", + "run", + "--vars-file", + vars_file, + "-s", + temp_dir, + yaml_file, + ] + ) + + # The "{injected_from_vars}" placeholder only matches the two running instances + # in the flight data once it is expanded from the vars file. + with open(os.path.join(temp_dir, policy_name, "resources.json")) as fh: + resources = json.load(fh) + self.assertEqual(len(resources), 2) + + def test_vars_file_invalid_path(self): + yaml_file = self.write_policy_file( + { + "policies": [ + {"name": "ec2-vars-missing", "resource": "ec2"} + ] + } + ) + self.run_and_expect_failure( + [ + "custodian", + "run", + "--vars-file", + "does-not-exist.json", + yaml_file, + ], + 2, + ) + + def test_vars_file_bad_contents(self): + temp_dir = self.get_temp_dir() + yaml_file = self.write_policy_file( + {"policies": [{"name": "ec2-vars-missing", "resource": "ec2"}]} + ) + bad_yaml_list = [ + "- list", # not mapping type + "key2: bad-key: value", # not valid yaml + ] + for bad_yaml in bad_yaml_list: + vars_file = self.write_raw_file(bad_yaml) + self.run_and_expect_failure( + [ + "custodian", + "run", + "--vars-file", + vars_file, + "-s", + temp_dir, + "--cache", + temp_dir + "/cache", + yaml_file, + ], + 1, + ) + def test_error(self): from c7n.policy import Policy diff --git a/tests/test_cw_dashboard.py b/tests/test_cw_dashboard.py new file mode 100644 index 00000000000..03052c7d6f9 --- /dev/null +++ b/tests/test_cw_dashboard.py @@ -0,0 +1,101 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from unittest import mock + +from botocore.exceptions import ClientError +from pytest_terraform import terraform + +from c7n.resources import cw + +from .common import BaseTest + + +@terraform('cloudwatch_dashboard_tag') +def test_cloudwatch_dashboard_tag(test, cloudwatch_dashboard_tag): + # Dashboards are tagged through the native cloudwatch apis, the + # resource groups tagging api rejects their region-less arns. + aws_region = 'us-east-1' + session_factory = test.replay_flight_data( + 'test_cloudwatch_dashboard_tag', region=aws_region) + + dashboard_name = cloudwatch_dashboard_tag[ + 'aws_cloudwatch_dashboard.test.dashboard_name'] + + p = test.load_policy({ + 'name': 'dashboard-tag', + 'resource': 'aws.cloudwatch-dashboard', + 'filters': [{'DashboardName': dashboard_name}], + 'actions': [ + {'type': 'tag', 'tags': {'App': 'Custodian', 'Env': 'Dev'}}]}, + session_factory=session_factory, config={'region': aws_region}) + resources = p.run() + test.assertEqual(len(resources), 1) + + client = session_factory().client('cloudwatch') + arn = resources[0]['DashboardArn'] + test.assertEqual( + {t['Key']: t['Value'] + for t in client.list_tags_for_resource(ResourceARN=arn)['Tags']}, + {'App': 'Custodian', 'Env': 'Dev'}) + + # filtering on the tag we just set exercises the read path - these + # tags aren't visible through the resource groups tagging api. + p = test.load_policy({ + 'name': 'dashboard-remove-tag', + 'resource': 'aws.cloudwatch-dashboard', + 'filters': [ + {'DashboardName': dashboard_name}, + {'tag:App': 'Custodian'}], + 'actions': [{'type': 'remove-tag', 'tags': ['Env']}]}, + session_factory=session_factory, config={'region': aws_region}) + resources = p.run() + test.assertEqual(len(resources), 1) + + test.assertEqual( + client.list_tags_for_resource(ResourceARN=arn)['Tags'], + [{'Key': 'App', 'Value': 'Custodian'}]) + + +def _client_error(code): + return ClientError({'Error': {'Code': code, 'Message': code}}, 'Operation') + + +class CloudWatchDashboardTest(BaseTest): + + def get_source(self): + p = self.load_policy({ + 'name': 'dashboard-augment', + 'resource': 'aws.cloudwatch-dashboard'}) + return p.resource_manager.source + + def test_augment_skips_deleted_dashboard(self): + source = self.get_source() + client = mock.MagicMock() + client.list_tags_for_resource.side_effect = [ + _client_error('ResourceNotFoundException'), + {'Tags': [{'Key': 'App', 'Value': 'Custodian'}]}, + ] + dashboards = [ + {'DashboardName': 'gone', + 'DashboardArn': 'arn:aws:cloudwatch::644160558196:dashboard/gone'}, + {'DashboardName': 'here', + 'DashboardArn': 'arn:aws:cloudwatch::644160558196:dashboard/here'}, + ] + with mock.patch.object(cw, 'local_session') as ls: + ls.return_value.client.return_value = client + resources = source.augment(dashboards) + self.assertEqual( + [r['DashboardName'] for r in resources], ['here']) + self.assertEqual( + resources[0]['Tags'], [{'Key': 'App', 'Value': 'Custodian'}]) + + def test_augment_error(self): + source = self.get_source() + client = mock.MagicMock() + client.list_tags_for_resource.side_effect = _client_error('AccessDeniedException') + with mock.patch.object(cw, 'local_session') as ls: + ls.return_value.client.return_value = client + with self.assertRaises(ClientError): + source.augment([ + {'DashboardName': 'here', + 'DashboardArn': 'arn:aws:cloudwatch::644160558196:dashboard/here'}]) diff --git a/tests/test_ebs.py b/tests/test_ebs.py index 2b74aef54d2..7a2abb7e0ff 100644 --- a/tests/test_ebs.py +++ b/tests/test_ebs.py @@ -177,16 +177,6 @@ def test_tag_error(self): tagger.process_resource_set(client, snaps, [{'Key': 'bar', 'Value': 'foo'}]) client.create_tags.assert_called_once() - def test_remove_snapshot(self): - snaps = [{'SnapshotId': 'a'}, {'SnapshotId': 'b'}, {'SnapshotId': 'c'}] - - t1 = list(snaps) - ErrorHandler.remove_snapshot('c', t1) - self.assertEqual([t['SnapshotId'] for t in t1], ['a', 'b']) - - ErrorHandler.remove_snapshot('d', snaps) - self.assertEqual(len(snaps), 3) - def test_get_bad_snapshot_malformed(self): operation_name = "DescribeSnapshots" error_response = { @@ -235,16 +225,6 @@ def test_get_bad_volume_notfound(self): vol = ErrorHandler.extract_bad_volume(e) self.assertEqual(vol, "vol-notfound") - def test_remove_volume(self): - vols = [{'VolumeId': 'a'}, {'VolumeId': 'b'}, {'VolumeId': 'c'}] - - t1 = list(vols) - ErrorHandler.remove_volume('c', t1) - self.assertEqual([t['VolumeId'] for t in t1], ['a', 'b']) - - ErrorHandler.remove_volume('d', vols) - self.assertEqual(len(vols), 3) - def test_volume_tag_error(self): vols = [{'VolumeId': 'vol-aa'}] error_response = { diff --git a/tests/test_eks.py b/tests/test_eks.py index 92b6d86564d..1b4d83bb5ff 100644 --- a/tests/test_eks.py +++ b/tests/test_eks.py @@ -183,6 +183,35 @@ def test_tag_and_remove_tag(self): name='devx')['cluster']['tags'], {'App': 'Custodian'}) + def test_tag_deleted_cluster(self): + # eks raises NotFoundException (not ResourceNotFoundException) when + # a cluster is gone by the time we tag it. No terraform fixture + # here - the cluster's absence is the whole point, so we tag an + # arn that isn't there rather than spend 20m creating a cluster + # to delete it. + aws_region = 'us-east-1' + factory = self.replay_flight_data( + 'test_eks_tag_deleted_cluster', region=aws_region) + + p = self.load_policy({ + 'name': 'eks-tag-deleted', + 'resource': 'aws.eks', + 'actions': [ + {'type': 'tag', 'tags': {'App': 'Custodian'}}, + {'type': 'remove-tag', 'tags': ['Env']}]}, + session_factory=factory, config={'region': aws_region}) + + gone = { + 'name': 'c7n-deleted', + 'arn': 'arn:aws:eks:%s:%s:cluster/c7n-deleted' % ( + aws_region, self.account_id), + 'tags': {'Env': 'Dev'}} + tag, remove_tag = p.resource_manager.actions + + # neither raises, the cluster is simply skipped + tag.process([gone]) + remove_tag.process([gone]) + def test_kms_filter(self): factory = self.replay_flight_data('test_eks_kms_filter') kms = factory().client('kms') diff --git a/tests/test_handler.py b/tests/test_handler.py index 1ed7afe1656..eac8e33f59a 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -56,6 +56,7 @@ def test_init_config_exec_option_merge(self): 'regions': ['us-east-1'], 'cache_period': 0, 'log_group': None, + 'vars_file': None, 'metrics': None}) def setupLambdaEnv( diff --git a/tests/test_kms.py b/tests/test_kms.py index f033e0660c1..0077eab7cb4 100644 --- a/tests/test_kms.py +++ b/tests/test_kms.py @@ -3,7 +3,7 @@ import json import time from datetime import datetime, timedelta -from unittest.mock import patch +from unittest.mock import patch, MagicMock import boto3 import moto @@ -1002,3 +1002,75 @@ def test_schedule_deletion_default_days(self): deletion_date = key_meta["DeletionDate"].astimezone(tzutc()) assert deletion_date > thirty_days_away assert deletion_date < (thirty_days_away + timedelta(days=1)) + + +class KMSSkipAwsManagedKeyTest(BaseTest): + # https://github.com/cloud-custodian/cloud-custodian/issues/11027 + # AWS managed keys (KeyManager == 'AWS') can't have their policy, + # rotation, or tags modified by any customer account - actions + # should skip them rather than let the API call fail. + + aws_key = { + 'KeyId': 'aws-key-1', + 'Arn': 'arn:aws:kms:us-east-1:111111111111:key/aws-key-1', + 'KeyManager': 'AWS', + } + customer_key = { + 'KeyId': 'cust-key-1', + 'Arn': 'arn:aws:kms:us-east-1:111111111111:key/cust-key-1', + 'KeyManager': 'CUSTOMER', + } + + def _policy(self, action, client_name='kms'): + mock_factory = MagicMock() + mock_factory.region = 'us-east-1' + client = mock_factory().client(client_name) + policy = self.load_policy( + {'name': 'k', 'resource': 'kms-key', 'actions': [action]}, + session_factory=mock_factory) + return policy, client + + def test_schedule_deletion_skips_aws_managed(self): + policy, client = self._policy({'type': 'schedule-deletion', 'days': 7}) + policy.resource_manager.actions[0].process([self.aws_key, self.customer_key]) + client.schedule_key_deletion.assert_called_once_with( + KeyId='cust-key-1', PendingWindowInDays=7) + + def test_schedule_deletion_all_aws_managed_noop(self): + policy, client = self._policy({'type': 'schedule-deletion'}) + policy.resource_manager.actions[0].process([self.aws_key]) + client.schedule_key_deletion.assert_not_called() + + def test_set_rotation_skips_aws_managed(self): + policy, client = self._policy({'type': 'set-rotation', 'state': True}) + policy.resource_manager.actions[0].process([self.aws_key, self.customer_key]) + client.enable_key_rotation.assert_called_once_with(KeyId='cust-key-1') + + def test_remove_statements_skips_aws_managed(self): + policy, client = self._policy({'type': 'remove-statements', 'statement_ids': 'matched'}) + policy.resource_manager.actions[0].process([self.aws_key]) + client.get_key_policy.assert_not_called() + + def _tag_policy(self, action): + mock_factory = MagicMock() + mock_factory.region = 'us-east-1' + tag_resources = mock_factory().client('resourcegroupstaggingapi').tag_resources + tag_resources.return_value = {} + policy = self.load_policy( + {'name': 'k', 'resource': 'kms-key', 'actions': [action]}, + session_factory=mock_factory) + return policy, tag_resources + + def test_tag_skips_aws_managed(self): + policy, tag_resources = self._tag_policy( + {'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process([self.aws_key, self.customer_key]) + tag_resources.assert_called_once_with( + ResourceARNList=['arn:aws:kms:us-east-1:111111111111:key/cust-key-1'], + Tags={'Owner': 'platform'}) + + def test_tag_all_aws_managed_noop(self): + policy, tag_resources = self._tag_policy( + {'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process([self.aws_key]) + tag_resources.assert_not_called() diff --git a/tests/test_networkmanager.py b/tests/test_networkmanager.py index 0c0072eac31..bd0b02b0c9f 100644 --- a/tests/test_networkmanager.py +++ b/tests/test_networkmanager.py @@ -203,7 +203,7 @@ def test_mark_for_op_global_network(self): def test_core_network_delete_error(self): invalid_network_id = 'core-network-7a6b617270696e736b69' - factory = self.record_flight_data("test_core_network_delete_error") + factory = self.replay_flight_data("test_core_network_delete_error") client = factory().client("networkmanager") mock_factory = MagicMock() mock_factory.region = 'us-east-1' diff --git a/tests/test_policy.py b/tests/test_policy.py index fda75b3ed21..046bc6eaf59 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -206,8 +206,9 @@ def test_resource_arn_override_generator(self): overrides = overrides.difference( {'account', 's3', 'hostedzone', 'log-group', 'rest-api', 'redshift-snapshot', 'rest-stage', 'codedeploy-app', 'codedeploy-group', 'fis-template', 'dlm-policy', - 'apigwv2', 'apigwv2-stage', 'lexv2-bot-alias', 'apigw-domain-name', 'fis-experiment', - 'launch-template-version', 'glue-table', 'glue-catalog', 'cloudwatch-synthetics'}) + 'apigwv2', 'apigwv2-route', 'apigwv2-stage', 'lexv2-bot-alias', 'apigw-domain-name', + 'fis-experiment', 'launch-template-version', 'glue-table', 'glue-catalog', + 'cloudwatch-synthetics'}) if overrides: raise ValueError("unknown arn overrides in %s" % (", ".join(overrides))) diff --git a/tests/test_route53.py b/tests/test_route53.py index 66c65acbeda..2771aa0e120 100644 --- a/tests/test_route53.py +++ b/tests/test_route53.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import time import logging +from unittest.mock import MagicMock import pytest from pytest_terraform import terraform @@ -698,3 +699,37 @@ def test_resolver_rule_query(self): resources = p.run() self.assertEqual(len(resources), 1) self.assertEqual(resources[0]["RuleType"], "FORWARD") + + # https://github.com/cloud-custodian/cloud-custodian/issues/11027 + # AWS's global auto-defined resolver rules (e.g. + # rslvr-autodefined-rr-internet-resolver) have no owning account and + # can't be tagged/modified by any customer - actions should skip them. + + auto_defined_rule = {'Id': 'rslvr-autodefined-rr-internet-resolver'} + customer_rule = {'Id': 'rslvr-rr-real1'} + + def _tag_policy(self, action): + mock_factory = MagicMock() + mock_factory.region = 'us-east-1' + tag_resources = mock_factory().client('resourcegroupstaggingapi').tag_resources + tag_resources.return_value = {} + policy = self.load_policy( + {'name': 'r', 'resource': 'resolver-rule', 'actions': [action]}, + session_factory=mock_factory) + return policy, tag_resources + + def test_tag_skips_auto_defined_rule(self): + policy, tag_resources = self._tag_policy( + {'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process( + [self.auto_defined_rule, self.customer_rule]) + tag_resources.assert_called_once() + arns = tag_resources.call_args.kwargs['ResourceARNList'] + self.assertEqual(len(arns), 1) + self.assertIn('rslvr-rr-real1', arns[0]) + + def test_tag_all_auto_defined_noop(self): + policy, tag_resources = self._tag_policy( + {'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process([self.auto_defined_rule]) + tag_resources.assert_not_called() diff --git a/tests/test_tags.py b/tests/test_tags.py index d303838f880..29cbc536ab3 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -9,6 +9,8 @@ from freezegun import freeze_time from unittest.mock import MagicMock, call, patch +from botocore.exceptions import ClientError + from c7n import tags as tagmod from c7n.tags import universal_retry, coalesce_copy_user_tags from c7n.exceptions import PolicyExecutionError, PolicyValidationError @@ -795,3 +797,183 @@ def test_universal_static_fast_path(self): {"K": "v"}) tag_resources.assert_called_once_with( ResourceARNList=["arn:x"], Tags={"K": "v"}) + + +# a syntactically valid instance id that doesn't exist, so ec2 answers the +# tag call with its real "resource gone" error rather than a malformed id one +GONE_INSTANCE_ID = 'i-0023456789abcdef0' + + +@terraform('ec2_instance_tag') +def test_native_tag_skips_gone_instance(test, ec2_instance_tag): + """A terminated instance in the batch shouldn't cost the rest their tags. + + ec2 fails the whole create_tags call when any resource in it is gone, + so we drop the ids it named and tag the rest. aws.ec2-instance-ami is + the resource from #10999 - it tags via instance id. + """ + aws_region = 'us-east-1' + session_factory = test.replay_flight_data( + 'test_native_tag_skips_gone_instance', region=aws_region) + + instance_ids = [ + ec2_instance_tag['aws_instance.first.id'], + ec2_instance_tag['aws_instance.second.id'], + ] + + p = test.load_policy({ + 'name': 'ec2-instance-ami-tag', + 'resource': 'aws.ec2-instance-ami', + 'actions': [ + {'type': 'tag', 'tags': {'App': 'Custodian'}}, + {'type': 'remove-tag', 'tags': ['App']}]}, + session_factory=session_factory, config={'region': aws_region}) + + resources = [r for r in p.resource_manager.resources() + if r['InstanceId'] in instance_ids] + test.assertEqual(len(resources), 2) + + # the batch ec2 rejects, plus the ones it should still tag + resource_set = resources + [{'InstanceId': GONE_INSTANCE_ID}] + tag, remove_tag = p.resource_manager.actions + + tag.process(resource_set) + client = session_factory().client('ec2') + tags = { + i['InstanceId']: {t['Key']: t['Value'] for t in i.get('Tags', [])} + for r in client.describe_instances( + InstanceIds=instance_ids)['Reservations'] + for i in r['Instances']} + for instance_id in instance_ids: + test.assertEqual(tags[instance_id].get('App'), 'Custodian') + + # and the same salvage on the way back out + remove_tag.id_key = 'InstanceId' + remove_tag.process(resource_set) + tags = { + i['InstanceId']: {t['Key']: t['Value'] for t in i.get('Tags', [])} + for r in client.describe_instances( + InstanceIds=instance_ids)['Reservations'] + for i in r['Instances']} + for instance_id in instance_ids: + test.assertNotIn('App', tags[instance_id]) + + +def test_universal_tag_skips_gone_snapshot(test): + """The rgta path passes through the service's own resource gone fault. + + elasticache reports a deleted snapshot as SnapshotNotFoundFault, which + universal_retry used to raise on - it only skipped + ResourceNotFoundException. No fixture, a snapshot that isn't there is + the point. + """ + aws_region = 'us-east-1' + session_factory = test.replay_flight_data( + 'test_universal_tag_skips_gone_snapshot', region=aws_region) + + p = test.load_policy({ + 'name': 'cache-snapshot-tag', + 'resource': 'aws.cache-snapshot', + 'actions': [{'type': 'tag', 'tags': {'App': 'Custodian'}}]}, + session_factory=session_factory, + config={'region': aws_region, 'account_id': test.account_id}) + + # doesn't raise, the snapshot is skipped + p.resource_manager.actions[0].process([{'SnapshotName': 'c7n-deleted'}]) + + +class ResourceGoneTest(BaseTest): + """Tag actions race with resource deletion, a resource which is gone by + the time we tag it should be skipped, not abort the whole resource set. + + The recorded tests above cover what aws actually answers with. These + cover our own control flow around it, which needs no api. + """ + + def client_error(self, code, message, op='CreateTags'): + return ClientError( + {'Error': {'Code': code, 'Message': message}}, op) + + def get_tag_action(self, name): + p = self.load_policy({ + 'name': 'tag-instances', + 'resource': 'aws.ec2', + 'actions': [ + {'type': 'tag', 'tags': {'Env': 'Dev'}}, + {'type': 'remove-tag', 'tags': ['Env']}]}) + action = {a.type: a for a in p.resource_manager.actions}[name] + action.id_key = 'InstanceId' + return action + + def test_extract_bad_resource_ids(self): + # message shapes here are the ones in our recorded api data, see + # tests/data/placebo - ec2 words them per resource type + for code, message, expected in ( + ('InvalidNetworkInterfaceID.NotFound', + "The networkInterface ID 'eni-d834cdcf' does not exist", + ('eni-d834cdcf',)), + ('InvalidVolume.NotFound', + "The volume 'vol-notfound' does not exist.", + ('vol-notfound',)), + ('InvalidSnapshotID.Malformed', + 'Invalid id: "snap-malformedsnap"', + ('snap-malformedsnap',)), + # not an ec2 bad id error, nothing to skip + ('UnauthorizedOperation', "You are not authorized", ()), + ('SnapshotNotFoundFault', "'snap-1' not found", ())): + self.assertEqual( + tagmod.extract_bad_resource_ids( + self.client_error(code, message)), + expected) + + def test_native_tag_raises_other_errors(self): + action = self.get_tag_action('tag') + client = MagicMock() + client.create_tags.side_effect = self.client_error( + 'UnauthorizedOperation', "You are not authorized") + self.assertRaises( + ClientError, + action.process_resource_set, + client, [{'InstanceId': 'i-1'}], [{'Key': 'Env', 'Value': 'Dev'}]) + + def test_native_tag_raises_on_unknown_bad_id(self): + # we only skip the resources ec2 actually named, an id we don't + # recognize means we don't understand the error - don't swallow it + action = self.get_tag_action('tag') + client = MagicMock() + client.create_tags.side_effect = self.client_error( + 'InvalidInstanceID.NotFound', + "The instance IDs 'i-9' do not exist") + self.assertRaises( + ClientError, + action.process_resource_set, + client, [{'InstanceId': 'i-1'}], [{'Key': 'Env', 'Value': 'Dev'}]) + + def test_universal_retry_skips_gone_and_retries_throttle(self): + # a throttle alongside a skip still retries the throttled arn + self.patch(time, "sleep", MagicMock()) + method = MagicMock() + method.side_effect = [ + {"FailedResourcesMap": { + "arn:abc": {"ErrorCode": "ThrottlingException"}, + "arn:def": {"ErrorCode": "NotFoundException"}}}, + {"Result": 32}, + ] + self.assertEqual( + universal_retry(method, ["arn:abc", "arn:def"]), {"Result": 32}) + self.assertEqual( + method.call_args_list, + [call(ResourceARNList=["arn:abc", "arn:def"]), + call(ResourceARNList=["arn:abc"])]) + + def test_universal_retry_all_skipped_stops(self): + # nothing left to retry, and the api rejects an empty arn list + sleep = MagicMock() + self.patch(time, "sleep", sleep) + method = MagicMock() + method.side_effect = [{ + "FailedResourcesMap": { + "arn:abc": {"ErrorCode": "SnapshotNotFoundFault"}}}] + universal_retry(method, ["arn:abc"]) + method.assert_called_once() + sleep.assert_not_called() diff --git a/tests/zpill.py b/tests/zpill.py index 8b526643b1d..267dd230f57 100644 --- a/tests/zpill.py +++ b/tests/zpill.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import base64 import fnmatch +import functools from io import BytesIO import json import os @@ -17,6 +18,7 @@ from placebo import pill from c7n.testing import CustodianTestCore +from c7n.utils import get_account_id_from_sts # Custodian Test Account. This is used only for testing. @@ -267,6 +269,11 @@ def save_response(self, service, operation, response_data, http_response=200): super(RedPill, self).save_response(service, operation, response_data, http_response) +@functools.lru_cache(maxsize=None) +def get_recording_account_id(): + return get_account_id_from_sts(boto3.Session()) + + class PillTest(CustodianTestCore): archive_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "placebo_data.zip") @@ -276,6 +283,19 @@ class PillTest(CustodianTestCore): recording = False + @property + def account_id(self): + """The account id to construct arns with. + + Recorded api data is sanitized to ACCOUNT_ID, but while recording + (or running functionally) we're talking to a real account and need + its real id, which is what the aws provider's initialization would + have resolved for us. + """ + if not self.recording: + return ACCOUNT_ID + return get_recording_account_id() + def cleanUp(self): self.pill = None diff --git a/tools/c7n_awscc/pyproject.toml b/tools/c7n_awscc/pyproject.toml index 17410b47693..a0d2e527a71 100644 --- a/tools/c7n_awscc/pyproject.toml +++ b/tools/c7n_awscc/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "jsonpatch<2.0,>=1.32", ] name = "c7n_awscc" -version = "0.1.38a1+e4c099e03" +version = "0.1.38a1+bf0b50dbc" readme = "readme.md" description = "Cloud Custodian - AWS Cloud Control Provider" classifiers = [ diff --git a/tools/c7n_azure/c7n_azure/resources/cosmos_db.py b/tools/c7n_azure/c7n_azure/resources/cosmos_db.py index cb0c77a73bc..4a207b4abb5 100644 --- a/tools/c7n_azure/c7n_azure/resources/cosmos_db.py +++ b/tools/c7n_azure/c7n_azure/resources/cosmos_db.py @@ -5,7 +5,7 @@ from itertools import groupby from azure.cosmos.cosmos_client import CosmosClient -from azure.cosmos.errors import HTTPFailure +from azure.cosmos.exceptions import CosmosHttpResponseError from azure.mgmt.cosmosdb.models import VirtualNetworkRule from c7n_azure import constants @@ -171,15 +171,32 @@ def get_cosmos_key(resource_group, resource_name, client, readonly=True): resource_name) return key_result.primary_readonly_master_key if readonly else key_result.primary_master_key + @staticmethod + @lru_cache() + def get_cosmos_data_client(url, key): + # Cached per (url, key): azure-cosmos v4's CosmosClient spawns a + # background endpoint health-check on every construction, so we + # reuse one client per account rather than building a fresh one + # (and its background thread) on every enumeration/action call. + data_client = CosmosClient(url, {'masterKey': key}) + # That health-check runs in a daemon thread kicked off from inside + # the constructor - block until it settles so callers (and test + # cassettes recorded against the synchronous v3 client) never race + # it. + refresh_thread = getattr( + getattr(data_client.client_connection, '_global_endpoint_manager', None), + '_refresh_thread', None) + if refresh_thread is not None: + refresh_thread.join(timeout=30) + return data_client + def get_data_client(self, parent_resource): key = CosmosDBChildResource.get_cosmos_key( parent_resource['resourceGroup'], parent_resource.get('name'), self.get_parent_manager().get_client()) - data_client = CosmosClient( - url_connection=parent_resource.get('properties').get('documentEndpoint'), - auth={'masterKey': key}) - return data_client + return CosmosDBChildResource.get_cosmos_data_client( + parent_resource.get('properties').get('documentEndpoint'), key) @resources.register('cosmosdb-database') @@ -202,8 +219,8 @@ def enumerate_resources(self, parent_resource, type_info, vault_url=None, **para data_client = self.get_data_client(parent_resource) try: - databases = list(data_client.ReadDatabases()) - except HTTPFailure as e: + databases = list(data_client.client_connection.ReadDatabases()) + except CosmosHttpResponseError as e: if e.status_code == 403: log.error("403 Forbidden. Ensure identity has `Cosmos DB Account Reader` or" "`DocumentDB Accounts Contributor` and that firewall is not " @@ -282,8 +299,8 @@ def enumerate_resources(self, parent_resource, type_info, vault_url=None, **para data_client = self.get_data_client(parent_resource) try: - databases = list(data_client.ReadDatabases()) - except HTTPFailure as e: + databases = list(data_client.client_connection.ReadDatabases()) + except CosmosHttpResponseError as e: if e.status_code == 403: log.error("403 Forbidden. Ensure identity has `Cosmos DB Account Reader` or" "`DocumentDB Accounts Contributor` and that firewall is not " @@ -293,7 +310,7 @@ def enumerate_resources(self, parent_resource, type_info, vault_url=None, **para collections = [] for d in databases: - container_result = list(data_client.ReadContainers(d['_self'])) + container_result = list(data_client.client_connection.ReadContainers(d['_self'])) for c in container_result: c.update({'c7n:document-endpoint': parent_resource.get('properties').get('documentEndpoint')}) @@ -457,7 +474,7 @@ def _process_resource(self, resource, account_client, throughput): new_offer = dict(offer) new_offer.pop('c7n:MatchedFilters', None) new_offer['content']['offerThroughput'] = throughput - account_client.ReplaceOffer(offer['_self'], new_offer) + account_client.client_connection.ReplaceOffer(offer['_self'], new_offer) @CosmosDBCollection.action_registry.register('restore-throughput-state') @@ -619,13 +636,12 @@ def get_cosmos_data_client_for_account(account_id, account_endpoint, manager, re manager.get_client(), readonly ) - data_client = CosmosClient(url_connection=account_endpoint, auth={'masterKey': key}) - return data_client + return CosmosDBChildResource.get_cosmos_data_client(account_endpoint, key) @staticmethod def populate_offer_data_for_account(resources, account_data_client): if not resources[0].get('c7n:offer'): - offers = list(account_data_client.ReadOffers()) + offers = list(account_data_client.client_connection.ReadOffers()) for resource in resources: offer = next((o for o in offers if o['offerResourceId'] == resource['_rid']), None) resource['c7n:offer'] = offer diff --git a/tools/c7n_azure/pyproject.toml b/tools/c7n_azure/pyproject.toml index c8f94ca1525..1676931bd90 100644 --- a/tools/c7n_azure/pyproject.toml +++ b/tools/c7n_azure/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "adal<2.0.0,>=1.2.6", "applicationinsights<1.0.0,>=0.11.9", "apscheduler<4.0.0,>=3.6.3", - "azure-cosmos<4.0.0,>=3.1.2", + "azure-cosmos>=3.1.2,<5.0.0", "azure-cosmosdb-table<2.0.0,>=1.0.6", "azure-functions<2.0.0,>=1.0.8", "azure-graphrbac<1.0.0,>=0.61.1", @@ -89,7 +89,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_azure" -version = "0.7.52a1+e4c099e03" +version = "0.7.52a1+bf0b50dbc" description = "Cloud Custodian - Azure Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_cosmos_db.py b/tools/c7n_azure/tests_azure/tests_resources/test_cosmos_db.py index e278daf7843..dd9e4ff707a 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_cosmos_db.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_cosmos_db.py @@ -1,6 +1,5 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 -from azure.cosmos.cosmos_client import CosmosClient from ..azure_common import BaseTest, arm_template, cassette_name from c7n_azure.resources.cosmos_db import (CosmosDBChildResource, CosmosDBFirewallRulesFilter, CosmosFirewallBypassFilter, @@ -554,19 +553,21 @@ def setUp(self, *args, **kwargs): account_name = "cctestcosmosdb%s" % sub_id key = CosmosDBChildResource.get_cosmos_key( 'test_cosmosdb', account_name, self.client, readonly=False) - self.data_client = CosmosClient( - url_connection='https://%s.documents.azure.com:443/' % account_name, - auth={ - 'masterKey': key - } - ) + # Reuse the same cached client the resources under test go through + # (see CosmosDBChildResource.get_cosmos_data_client) rather than + # constructing a separate one: azure-cosmos v4 spawns a background + # endpoint health-check on every CosmosClient construction, and a + # second, uncached client here would race that background call + # against whichever cassette happens to be active at the time. + self.data_client = CosmosDBChildResource.get_cosmos_data_client( + 'https://%s.documents.azure.com:443/' % account_name, key) self.offer = None def tearDown(self, *args, **kwargs): super(CosmosDBThroughputActionsTest, self).tearDown(*args, **kwargs) if self.offer: self.offer['content']['offerThroughput'] = 400 - self.data_client.ReplaceOffer( + self.data_client.client_connection.ReplaceOffer( self.offer['_self'], self.offer ) @@ -635,7 +636,7 @@ def test_restore_throughput_state_updates_throughput_from_tag(self): collection_offer['content']['offerThroughput'] = throughput_to_restore + 100 - self.data_client.ReplaceOffer( + self.data_client.client_connection.ReplaceOffer( collection_offer['_self'], collection_offer ) @@ -668,7 +669,7 @@ def test_restore_throughput_state_updates_throughput_from_tag(self): def _assert_offer_throughput_equals(self, throughput, resource_self): self.sleep_in_live_mode() - offers = self.data_client.ReadOffers() + offers = self.data_client.client_connection.ReadOffers() offer = next((o for o in offers if o['resource'] == resource_self), None) self.assertIsNotNone(offer) self.assertEqual(throughput, offer['content']['offerThroughput']) diff --git a/tools/c7n_gcp/c7n_gcp/actions/core.py b/tools/c7n_gcp/c7n_gcp/actions/core.py index 8fc711adb90..4093a779cef 100644 --- a/tools/c7n_gcp/c7n_gcp/actions/core.py +++ b/tools/c7n_gcp/c7n_gcp/actions/core.py @@ -6,6 +6,8 @@ from c7n.actions import Action as BaseAction from c7n.utils import local_session, chunks +from c7n_gcp.query import TypeInfo + class Action(BaseAction): pass @@ -112,7 +114,21 @@ def get_permissions(self): def get_operation_name(self, model, resource): return self.method_spec['op'] - def get_resource_params(self, model, resource): + def get_resource_params(self, model: TypeInfo, resource: dict) -> dict: + """Build the API call parameters for a single method call. + + Subclasses must implement this. The return value is passed + straight through to ``invoke_api`` and then + ``client.execute_command(op_name, params)``, so its required + keys are whatever the GCP API method named by + ``method_spec['op']`` expects (e.g. ``name``, ``body``, and + ``updateMask`` for a ``patch`` call) -- there is no schema + enforcing this shape. + + :param model: the resource's ``resource_type`` (a ``TypeInfo`` + subclass), as returned by ``self.manager.get_model()``. + :param resource: the resource dict being acted on. + """ raise NotImplementedError("subclass responsibility") def get_client(self, session, model): diff --git a/tools/c7n_gcp/c7n_gcp/region.py b/tools/c7n_gcp/c7n_gcp/region.py index ed0ac0a15e0..cf36166f9d9 100644 --- a/tools/c7n_gcp/c7n_gcp/region.py +++ b/tools/c7n_gcp/c7n_gcp/region.py @@ -39,6 +39,9 @@ def __init__(self, ctx=None, data=()): def get_permissions(self): return () + def get_resource(self, resource_info): + return {'name': resource_info['name']} + @classmethod def set_regions(cls, regions): # test helper diff --git a/tools/c7n_gcp/c7n_gcp/resources/dataproc.py b/tools/c7n_gcp/c7n_gcp/resources/dataproc.py index 69e73f52215..dd7f2c1c8b3 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/dataproc.py +++ b/tools/c7n_gcp/c7n_gcp/resources/dataproc.py @@ -1,6 +1,8 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import re + from c7n_gcp.filters import IamPolicyFilter from c7n_gcp.filters.iampolicy import IamPolicyValueFilter from c7n_gcp.query import ChildResourceManager, ChildTypeInfo @@ -21,6 +23,9 @@ class resource_type(ChildTypeInfo): 'resource': 'region', 'child_enum_params': { ('name', 'region')}, + 'parent_get_params': [ + ('labels."goog-dataproc-location"', 'name'), + ], 'use_child_query': True, } default_report_fields = ['clusterName', 'status.state', 'status.stateStartTime'] @@ -32,6 +37,22 @@ class resource_type(ChildTypeInfo): def _get_location(cls, resource): return resource['labels']['goog-dataproc-location'] + @staticmethod + def get(client, resource_info): + resource_name = resource_info['resourceName'] + if match := re.match( + '(?:.*/)?projects/([^/]+)/regions/([^/]+)/clusters/([^/]+)$', + resource_name): + project_id, region, cluster_name = match.groups() + return client.execute_query( + 'get', {'projectId': project_id, + 'region': region, + 'clusterName': cluster_name}) + + raise ValueError( + f"Couldn't parse project, region, and cluster " + f"from resource name, {resource_name}") + @DataprocClusters.filter_registry.register('iam-policy') class DataprocClustersIamPolicyFilter(IamPolicyFilter): diff --git a/tools/c7n_gcp/c7n_gcp/resources/kms.py b/tools/c7n_gcp/c7n_gcp/resources/kms.py index 646ecd46f35..c7584e6f133 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/kms.py +++ b/tools/c7n_gcp/c7n_gcp/resources/kms.py @@ -3,11 +3,12 @@ import re +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from c7n.utils import local_session, type_schema from c7n_gcp.provider import resources from c7n_gcp.query import QueryResourceManager, TypeInfo, ChildResourceManager, ChildTypeInfo, \ GcpLocation -from c7n_gcp.actions import SetIamPolicy +from c7n_gcp.actions import MethodAction, SetIamPolicy from c7n_gcp.filters import IamPolicyFilter from c7n.filters import Filter @@ -134,6 +135,112 @@ def _get_location(cls, resource): return resource["name"].split('/')[3] +@KmsCryptoKey.action_registry.register('update') +class KmsCryptoKeyUpdate(MethodAction): + """Update mutable properties of a gcp.kms-cryptokey resource. + + GCP action is https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/patch + + ``rotationPeriod``/``nextRotationTime`` are only valid for keys with + ``purpose`` ``ENCRYPT_DECRYPT`` (enforced at run time, not by schema). + + :Example: + + .. code-block:: yaml + + policies: + - name: gcp-kms-cryptokey-set-rotation + resource: gcp.kms-cryptokey + actions: + - type: update + rotationPeriod: 7776000s + nextRotationTime: "2027-01-01T00:00:00Z" + """ + + rotation_fields = frozenset(('rotationPeriod', 'nextRotationTime')) + + schema = type_schema( + 'update', + **{ + 'rotationPeriod': {'type': 'string'}, + 'nextRotationTime': {'type': 'string'}, + 'versionTemplate': { + 'type': 'object', + 'minProperties': 1, + 'additionalProperties': False, + 'properties': { + 'algorithm': {'type': 'string'}, + }, + }, + 'keyAccessJustificationsPolicy': { + 'type': 'object', + 'minProperties': 1, + 'additionalProperties': False, + 'properties': { + 'allowedAccessReasons': { + 'type': 'array', + 'minItems': 1, + 'items': { + 'enum': [ + 'REASON_UNSPECIFIED', + 'CUSTOMER_INITIATED_SUPPORT', + 'GOOGLE_INITIATED_SERVICE', + 'THIRD_PARTY_DATA_REQUEST', + 'GOOGLE_INITIATED_REVIEW', + 'CUSTOMER_INITIATED_ACCESS', + 'GOOGLE_INITIATED_SYSTEM_OPERATION', + 'REASON_NOT_EXPECTED', + 'MODIFIED_CUSTOMER_INITIATED_ACCESS', + 'MODIFIED_GOOGLE_INITIATED_SYSTEM_OPERATION', + 'GOOGLE_RESPONSE_TO_PRODUCTION_ALERT', + 'CUSTOMER_AUTHORIZED_WORKFLOW_SERVICING', + ], + }, + }, + }, + }, + }) + schema['dependencies'] = { + 'rotationPeriod': ['nextRotationTime'], + } + method_spec = {'op': 'patch'} + method_perm = 'update' + + def validate(self): + super().validate() + if not (self.data.keys() - {'type'}): + raise PolicyValidationError( + "policy:%s action:%s requires at least one field to update" % ( + self.manager.ctx.policy.name, self.type)) + return self + + def process_resource_set(self, client, model, resources): + if self.rotation_fields & self.data.keys(): + invalid = [ + r['name'] for r in resources + if r.get('purpose') != 'ENCRYPT_DECRYPT' + ] + if invalid: + raise PolicyExecutionError( + "Automatic rotation requires CryptoKeys with purpose " + "ENCRYPT_DECRYPT; found: %s" % ', '.join(invalid)) + return super().process_resource_set(client, model, resources) + + def get_resource_params(self, model, resource): + fields = {k: v for k, v in self.data.items() if k != 'type'} + mask_paths = [] + for key, value in fields.items(): + if isinstance(value, dict): + mask_paths.extend('%s.%s' % (key, subkey) for subkey in value) + else: + mask_paths.append(key) + return { + 'name': resource['name'], + 'body': fields, + 'updateMask': ','.join(mask_paths), + } + + @KmsCryptoKey.filter_registry.register('iam-policy') class KmsCryptokeyIamPolicyFilter(IamPolicyFilter): """ diff --git a/tools/c7n_gcp/pyproject.toml b/tools/c7n_gcp/pyproject.toml index 82bea832862..e982722307b 100644 --- a/tools/c7n_gcp/pyproject.toml +++ b/tools/c7n_gcp/pyproject.toml @@ -10,12 +10,12 @@ dependencies = [ "google-auth<3.0.0,>=2.1.0", "google-cloud-logging<4.0,>=3.2", "google-cloud-monitoring<3.0.0,>=2.5.0", - "google-cloud-storage<3.0,>=2.7", + "google-cloud-storage>=2.7,<4.0", "pyrate-limiter<3.0.0,>=2.8.4", "retrying<2.0.0,>=1.3.3", ] name = "c7n_gcp" -version = "0.4.52a1+e4c099e03" +version = "0.4.52a1+bf0b50dbc" description = "Cloud Custodian - Google Cloud Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_gcp/tests/data/flights/dataproc-clusters-get/get-v1-projects-cloud-custodian-regions-us-east1-clusters-c7n-test-f4c79164_1.json b/tools/c7n_gcp/tests/data/flights/dataproc-clusters-get/get-v1-projects-cloud-custodian-regions-us-east1-clusters-c7n-test-f4c79164_1.json new file mode 100644 index 00000000000..ef2f955389e --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/dataproc-clusters-get/get-v1-projects-cloud-custodian-regions-us-east1-clusters-c7n-test-f4c79164_1.json @@ -0,0 +1,153 @@ +{ + "headers": { + "x-debug-tracking-id": "1864427333449713756;o=0", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Tue, 14 Jul 2026 14:31:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "6380", + "-content-encoding": "gzip", + "content-location": "https://dataproc.googleapis.com/v1/projects/cloud-custodian/regions/us-east1/clusters/c7n-test-f4c79164?alt=json" + }, + "body": { + "projectId": "cloud-custodian", + "clusterName": "c7n-test-f4c79164", + "config": { + "configBucket": "dataproc-staging-us-east1-693936050605-v0rw1hor", + "tempBucket": "dataproc-temp-us-east1-693936050605-xolpqygx", + "gceClusterConfig": { + "zoneUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-east1-b", + "networkUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/default", + "serviceAccountScopes": [ + "https://www.googleapis.com/auth/cloud-platform" + ], + "internalIpOnly": false, + "shieldedInstanceConfig": { + "enableSecureBoot": true, + "enableVtpm": true, + "enableIntegrityMonitoring": true + } + }, + "masterConfig": { + "numInstances": 1, + "instanceNames": [ + "c7n-test-f4c79164-m" + ], + "imageUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/images/dataproc-2-2-deb12-20260619-045100-rc01", + "machineTypeUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-east1-b/machineTypes/e2-medium", + "diskConfig": { + "bootDiskSizeGb": 30, + "bootDiskType": "pd-standard" + }, + "minCpuPlatform": "AUTOMATIC", + "preemptibility": "NON_PREEMPTIBLE" + }, + "workerConfig": { + "numInstances": 2, + "instanceNames": [ + "c7n-test-f4c79164-w-0", + "c7n-test-f4c79164-w-1" + ], + "imageUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/images/dataproc-2-2-deb12-20260619-045100-rc01", + "machineTypeUri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-east1-b/machineTypes/e2-medium", + "diskConfig": { + "bootDiskSizeGb": 30, + "bootDiskType": "pd-standard" + }, + "minCpuPlatform": "AUTOMATIC", + "preemptibility": "NON_PREEMPTIBLE" + }, + "softwareConfig": { + "imageVersion": "2.2.84-debian12", + "properties": { + "capacity-scheduler:yarn.scheduler.capacity.resource-calculator": "org.apache.hadoop.yarn.util.resource.DominantResourceCalculator", + "capacity-scheduler:yarn.scheduler.capacity.root.default.ordering-policy": "fair", + "core:fs.gs.block.size": "134217728", + "core:fs.gs.metadata.cache.enable": "false", + "core:fs.gs.operation.move.enable": "true", + "core:hadoop.ssl.enabled.protocols": "TLSv1,TLSv1.1,TLSv1.2", + "distcp:mapreduce.map.java.opts": "-Xmx576m", + "distcp:mapreduce.map.memory.mb": "768", + "distcp:mapreduce.reduce.java.opts": "-Xmx576m", + "distcp:mapreduce.reduce.memory.mb": "768", + "hadoop-env:HADOOP_DATANODE_OPTS": "-Xmx512m", + "hdfs:dfs.datanode.address": "0.0.0.0:9866", + "hdfs:dfs.datanode.http.address": "0.0.0.0:9864", + "hdfs:dfs.datanode.https.address": "0.0.0.0:9865", + "hdfs:dfs.datanode.ipc.address": "0.0.0.0:9867", + "hdfs:dfs.namenode.handler.count": "20", + "hdfs:dfs.namenode.http-address": "0.0.0.0:9870", + "hdfs:dfs.namenode.https-address": "0.0.0.0:9871", + "hdfs:dfs.namenode.lifeline.rpc-address": "c7n-test-f4c79164-m:8050", + "hdfs:dfs.namenode.secondary.http-address": "0.0.0.0:9868", + "hdfs:dfs.namenode.secondary.https-address": "0.0.0.0:9869", + "hdfs:dfs.namenode.service.handler.count": "10", + "hdfs:dfs.namenode.servicerpc-address": "c7n-test-f4c79164-m:8051", + "mapred-env:HADOOP_JOB_HISTORYSERVER_HEAPSIZE": "1024", + "mapred:mapreduce.job.maps": "9", + "mapred:mapreduce.job.reduce.slowstart.completedmaps": "0.95", + "mapred:mapreduce.job.reduces": "1", + "mapred:mapreduce.jobhistory.recovery.store.class": "org.apache.hadoop.mapreduce.v2.hs.HistoryServerLeveldbStateStoreService", + "mapred:mapreduce.map.cpu.vcores": "1", + "mapred:mapreduce.map.java.opts": "-Xmx1311m", + "mapred:mapreduce.map.memory.mb": "1639", + "mapred:mapreduce.reduce.cpu.vcores": "2", + "mapred:mapreduce.reduce.java.opts": "-Xmx2622m", + "mapred:mapreduce.reduce.memory.mb": "3278", + "mapred:mapreduce.task.io.sort.mb": "256", + "mapred:yarn.app.mapreduce.am.command-opts": "-Xmx1311m", + "mapred:yarn.app.mapreduce.am.resource.cpu-vcores": "1", + "mapred:yarn.app.mapreduce.am.resource.mb": "1639", + "spark-env:SPARK_DAEMON_MEMORY": "1024m", + "spark:spark.driver.maxResultSize": "512m", + "spark:spark.driver.memory": "1024m", + "spark:spark.executor.cores": "1", + "spark:spark.executor.instances": "2", + "spark:spark.executor.memory": "1255m", + "spark:spark.executorEnv.OPENBLAS_NUM_THREADS": "1", + "spark:spark.plugins.defaultList": "com.google.cloud.dataproc.DataprocSparkPlugin", + "spark:spark.scheduler.mode": "FAIR", + "spark:spark.sql.cbo.enabled": "true", + "spark:spark.sql.optimizer.runtime.bloomFilter.join.pattern.enabled": "true", + "spark:spark.ui.port": "0", + "spark:spark.yarn.am.memory": "640m", + "yarn-env:YARN_NODEMANAGER_HEAPSIZE": "409", + "yarn-env:YARN_RESOURCEMANAGER_HEAPSIZE": "1024", + "yarn-env:YARN_TIMELINESERVER_HEAPSIZE": "1024", + "yarn:yarn.nodemanager.address": "0.0.0.0:8026", + "yarn:yarn.nodemanager.resource.cpu-vcores": "2", + "yarn:yarn.nodemanager.resource.memory-mb": "3278", + "yarn:yarn.resourcemanager.decommissioning-nodes-watcher.decommission-if-no-shuffle-data": "true", + "yarn:yarn.resourcemanager.nodemanager-graceful-decommission-timeout-secs": "86400", + "yarn:yarn.scheduler.maximum-allocation-mb": "3278", + "yarn:yarn.scheduler.minimum-allocation-mb": "1" + } + }, + "endpointConfig": {} + }, + "status": { + "state": "RUNNING", + "stateStartTime": "2026-07-14T14:31:44.098167Z" + }, + "clusterUuid": "879e0c1f-07cc-419f-8a29-918e62c6a80a", + "statusHistory": [ + { + "state": "CREATING", + "stateStartTime": "2026-07-14T14:25:46.372880Z" + } + ], + "labels": { + "goog-dataproc-cluster-name": "c7n-test-f4c79164", + "goog-dataproc-cluster-uuid": "879e0c1f-07cc-419f-8a29-918e62c6a80a", + "goog-dataproc-location": "us-east1", + "goog-dataproc-cluster-create-timestamp": "1784039146", + "goog-dataproc-drz-resource-uuid": "cluster-879e0c1f-07cc-419f-8a29-918e62c6a80a" + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json new file mode 100644 index 00000000000..cb8326a5f6c --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json @@ -0,0 +1,41 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 13 Jul 2026 18:58:37 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "899", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey?alt=json" + }, + "body": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-07-13T18:58:35.470163471Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-13T18:58:35.470163471Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-07-13T18:58:35.470163471Z", + "nextRotationTime": "2027-01-01T00:00:00Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "default" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys_1.json new file mode 100644 index 00000000000..418223c76a3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys_1.json @@ -0,0 +1,44 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 13 Jul 2026 18:58:36 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "953", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-07-13T18:58:35.470163471Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-13T18:58:35.470163471Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-07-13T18:58:35.470163471Z", + "labels": { + "goog-terraform-provisioned": "true", + "env": "default" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json new file mode 100644 index 00000000000..2cb5644f55d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/get-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json @@ -0,0 +1,26 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 13 Jul 2026 18:58:36 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "205", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings?alt=json" + }, + "body": { + "keyRings": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish", + "createTime": "2026-07-13T18:58:35.221652589Z" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/patch-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/patch-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json new file mode 100644 index 00000000000..9a5bed6d70a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-cryptokey-update/patch-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-redfish-cryptoKeys-c7n-test-cryptokey_1.json @@ -0,0 +1,40 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 13 Jul 2026 18:58:37 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "899", + "-content-encoding": "gzip" + }, + "body": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-07-13T18:58:35.470163471Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-13T18:58:35.470163471Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-07-13T18:58:35.470163471Z", + "nextRotationTime": "2027-01-01T00:00:00Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "default", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/dataproc_cluster/main.tf b/tools/c7n_gcp/tests/terraform/dataproc_cluster/main.tf new file mode 100644 index 00000000000..51808b8c34a --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/dataproc_cluster/main.tf @@ -0,0 +1,43 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id + billing_project = var.google_project_id + user_project_override = true +} + +resource "random_id" "suffix" { + byte_length = 4 +} + +resource "google_dataproc_cluster" "default" { + name = "c7n-test-${random_id.suffix.hex}" + region = "us-east1" + project = var.google_project_id + + cluster_config { + staging_bucket = null + + master_config { + num_instances = 1 + machine_type = "e2-medium" + disk_config { + boot_disk_size_gb = 30 + } + } + + worker_config { + num_instances = 2 + machine_type = "e2-medium" + disk_config { + boot_disk_size_gb = 30 + } + } + + gce_cluster_config { + zone = "us-east1-b" + } + } +} diff --git a/tools/c7n_gcp/tests/terraform/dataproc_cluster/tf_resources.json b/tools/c7n_gcp/tests/terraform/dataproc_cluster/tf_resources.json new file mode 100644 index 00000000000..e01e5a38a26 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/dataproc_cluster/tf_resources.json @@ -0,0 +1,240 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_dataproc_cluster": { + "default": { + "cluster_config": [ + { + "autoscaling_config": [], + "auxiliary_node_groups": [], + "bucket": "dataproc-staging-us-east1-693936050605-v0rw1hor", + "cluster_tier": "", + "cluster_type": "", + "dataproc_metric_config": [], + "encryption_config": [], + "endpoint_config": [ + { + "enable_http_port_access": false, + "http_ports": {} + } + ], + "engine": "", + "gce_cluster_config": [ + { + "confidential_instance_config": [], + "internal_ip_only": false, + "metadata": {}, + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/default", + "node_group_affinity": [], + "reservation_affinity": [], + "resource_manager_tags": {}, + "service_account": "", + "service_account_scopes": [ + "https://www.googleapis.com/auth/cloud-platform" + ], + "shielded_instance_config": [ + { + "enable_integrity_monitoring": true, + "enable_secure_boot": true, + "enable_vtpm": true + } + ], + "subnetwork": "", + "tags": null, + "zone": "us-east1-b" + } + ], + "initialization_action": [], + "lifecycle_config": [], + "master_config": [ + { + "accelerators": [], + "disk_config": [ + { + "boot_disk_provisioned_iops": 0, + "boot_disk_provisioned_throughput": 0, + "boot_disk_size_gb": 30, + "boot_disk_type": "pd-standard", + "local_ssd_interface": "", + "num_local_ssds": 0 + } + ], + "image_uri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/images/dataproc-2-2-deb12-20260619-045100-rc01", + "instance_flexibility_policy": [ + { + "instance_selection_list": null, + "instance_selection_results": null + } + ], + "instance_names": [ + "c7n-test-f4c79164-m" + ], + "machine_type": "e2-medium", + "min_cpu_platform": "AUTOMATIC", + "num_instances": 1 + } + ], + "metastore_config": [], + "preemptible_worker_config": [ + { + "disk_config": [ + { + "boot_disk_provisioned_iops": null, + "boot_disk_provisioned_throughput": null, + "boot_disk_size_gb": null, + "boot_disk_type": null, + "local_ssd_interface": null, + "num_local_ssds": null + } + ], + "instance_flexibility_policy": [ + { + "instance_selection_list": null, + "instance_selection_results": null, + "provisioning_model_mix": null + } + ], + "instance_names": [], + "num_instances": 0, + "preemptibility": "" + } + ], + "security_config": [], + "software_config": [ + { + "image_version": "2.2.84-debian12", + "optional_components": [], + "override_properties": {}, + "properties": { + "capacity-scheduler:yarn.scheduler.capacity.resource-calculator": "org.apache.hadoop.yarn.util.resource.DominantResourceCalculator", + "capacity-scheduler:yarn.scheduler.capacity.root.default.ordering-policy": "fair", + "core:fs.gs.block.size": "134217728", + "core:fs.gs.metadata.cache.enable": "false", + "core:fs.gs.operation.move.enable": "true", + "core:hadoop.ssl.enabled.protocols": "TLSv1,TLSv1.1,TLSv1.2", + "distcp:mapreduce.map.java.opts": "-Xmx576m", + "distcp:mapreduce.map.memory.mb": "768", + "distcp:mapreduce.reduce.java.opts": "-Xmx576m", + "distcp:mapreduce.reduce.memory.mb": "768", + "hadoop-env:HADOOP_DATANODE_OPTS": "-Xmx512m", + "hdfs:dfs.datanode.address": "0.0.0.0:9866", + "hdfs:dfs.datanode.http.address": "0.0.0.0:9864", + "hdfs:dfs.datanode.https.address": "0.0.0.0:9865", + "hdfs:dfs.datanode.ipc.address": "0.0.0.0:9867", + "hdfs:dfs.namenode.handler.count": "20", + "hdfs:dfs.namenode.http-address": "0.0.0.0:9870", + "hdfs:dfs.namenode.https-address": "0.0.0.0:9871", + "hdfs:dfs.namenode.lifeline.rpc-address": "c7n-test-f4c79164-m:8050", + "hdfs:dfs.namenode.secondary.http-address": "0.0.0.0:9868", + "hdfs:dfs.namenode.secondary.https-address": "0.0.0.0:9869", + "hdfs:dfs.namenode.service.handler.count": "10", + "hdfs:dfs.namenode.servicerpc-address": "c7n-test-f4c79164-m:8051", + "mapred-env:HADOOP_JOB_HISTORYSERVER_HEAPSIZE": "1024", + "mapred:mapreduce.job.maps": "9", + "mapred:mapreduce.job.reduce.slowstart.completedmaps": "0.95", + "mapred:mapreduce.job.reduces": "1", + "mapred:mapreduce.jobhistory.recovery.store.class": "org.apache.hadoop.mapreduce.v2.hs.HistoryServerLeveldbStateStoreService", + "mapred:mapreduce.map.cpu.vcores": "1", + "mapred:mapreduce.map.java.opts": "-Xmx1311m", + "mapred:mapreduce.map.memory.mb": "1639", + "mapred:mapreduce.reduce.cpu.vcores": "2", + "mapred:mapreduce.reduce.java.opts": "-Xmx2622m", + "mapred:mapreduce.reduce.memory.mb": "3278", + "mapred:mapreduce.task.io.sort.mb": "256", + "mapred:yarn.app.mapreduce.am.command-opts": "-Xmx1311m", + "mapred:yarn.app.mapreduce.am.resource.cpu-vcores": "1", + "mapred:yarn.app.mapreduce.am.resource.mb": "1639", + "spark-env:SPARK_DAEMON_MEMORY": "1024m", + "spark:spark.driver.maxResultSize": "512m", + "spark:spark.driver.memory": "1024m", + "spark:spark.executor.cores": "1", + "spark:spark.executor.instances": "2", + "spark:spark.executor.memory": "1255m", + "spark:spark.executorEnv.OPENBLAS_NUM_THREADS": "1", + "spark:spark.plugins.defaultList": "com.google.cloud.dataproc.DataprocSparkPlugin", + "spark:spark.scheduler.mode": "FAIR", + "spark:spark.sql.cbo.enabled": "true", + "spark:spark.sql.optimizer.runtime.bloomFilter.join.pattern.enabled": "true", + "spark:spark.ui.port": "0", + "spark:spark.yarn.am.memory": "640m", + "yarn-env:YARN_NODEMANAGER_HEAPSIZE": "409", + "yarn-env:YARN_RESOURCEMANAGER_HEAPSIZE": "1024", + "yarn-env:YARN_TIMELINESERVER_HEAPSIZE": "1024", + "yarn:yarn.nodemanager.address": "0.0.0.0:8026", + "yarn:yarn.nodemanager.resource.cpu-vcores": "2", + "yarn:yarn.nodemanager.resource.memory-mb": "3278", + "yarn:yarn.resourcemanager.decommissioning-nodes-watcher.decommission-if-no-shuffle-data": "true", + "yarn:yarn.resourcemanager.nodemanager-graceful-decommission-timeout-secs": "86400", + "yarn:yarn.scheduler.maximum-allocation-mb": "3278", + "yarn:yarn.scheduler.minimum-allocation-mb": "1" + } + } + ], + "staging_bucket": "", + "temp_bucket": "dataproc-temp-us-east1-693936050605-xolpqygx", + "worker_config": [ + { + "accelerators": [], + "disk_config": [ + { + "boot_disk_provisioned_iops": 0, + "boot_disk_provisioned_throughput": 0, + "boot_disk_size_gb": 30, + "boot_disk_type": "pd-standard", + "local_ssd_interface": "", + "num_local_ssds": 0 + } + ], + "image_uri": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/images/dataproc-2-2-deb12-20260619-045100-rc01", + "instance_flexibility_policy": [ + { + "instance_selection_list": null, + "instance_selection_results": null + } + ], + "instance_names": [ + "c7n-test-f4c79164-w-0", + "c7n-test-f4c79164-w-1" + ], + "machine_type": "e2-medium", + "min_cpu_platform": "AUTOMATIC", + "min_num_instances": 0, + "num_instances": 2 + } + ] + } + ], + "deletion_policy": "DELETE", + "effective_labels": { + "goog-dataproc-cluster-create-timestamp": "1784039146", + "goog-dataproc-cluster-name": "c7n-test-f4c79164", + "goog-dataproc-cluster-uuid": "879e0c1f-07cc-419f-8a29-918e62c6a80a", + "goog-dataproc-drz-resource-uuid": "cluster-879e0c1f-07cc-419f-8a29-918e62c6a80a", + "goog-dataproc-location": "us-east1" + }, + "graceful_decommission_timeout": "0s", + "id": "projects/cloud-custodian/regions/us-east1/clusters/c7n-test-f4c79164", + "labels": null, + "name": "c7n-test-f4c79164", + "project": "cloud-custodian", + "region": "us-east1", + "terraform_labels": {}, + "timeouts": null, + "virtual_cluster_config": [] + } + }, + "random_id": { + "suffix": { + "b64_std": "9MeRZA==", + "b64_url": "9MeRZA", + "byte_length": 4, + "dec": "4106719588", + "hex": "f4c79164", + "id": "9MeRZA", + "keepers": null, + "prefix": null + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/kms_cryptokey/tf_resources.json b/tools/c7n_gcp/tests/terraform/kms_cryptokey/tf_resources.json new file mode 100644 index 00000000000..482a5ccdcba --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/kms_cryptokey/tf_resources.json @@ -0,0 +1,62 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_kms_crypto_key": { + "c7n_test_key": { + "crypto_key_backend": "", + "deletion_policy": "DELETE", + "destroy_scheduled_duration": "2592000s", + "effective_labels": { + "env": "default", + "goog-terraform-provisioned": "true" + }, + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey", + "import_only": false, + "key_ring": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish", + "labels": { + "env": "default" + }, + "name": "c7n-test-cryptokey", + "primary": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED" + } + ], + "purpose": "ENCRYPT_DECRYPT", + "rotation_period": "", + "skip_initial_version_creation": false, + "terraform_labels": { + "env": "default", + "goog-terraform-provisioned": "true" + }, + "timeouts": null, + "version_template": [ + { + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "protection_level": "SOFTWARE" + } + ] + } + }, + "google_kms_key_ring": { + "c7n_test": { + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-redfish", + "location": "us-central1", + "name": "c7n-test-keyring-redfish", + "project": "stkl-jim-fulton", + "timeouts": null + } + }, + "random_pet": { + "keyring": { + "id": "redfish", + "keepers": null, + "length": 1, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_dataproc.py b/tools/c7n_gcp/tests/test_dataproc.py index fc90b840fc5..689a2b5d9a2 100644 --- a/tools/c7n_gcp/tests/test_dataproc.py +++ b/tools/c7n_gcp/tests/test_dataproc.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from gcp_common import BaseTest +from pytest_terraform import terraform class DataprocTest(BaseTest): @@ -29,6 +30,24 @@ def test_dataproc_clusters_filter_iam_query(self): self.assertEqual('cluster-8065', resources[0]['clusterName']) +@terraform('dataproc_cluster') +def test_dataproc_clusters_get(test, dataproc_cluster): + project_id = dataproc_cluster['google_dataproc_cluster.default.project'] + region = dataproc_cluster['google_dataproc_cluster.default.region'] + cluster_name = dataproc_cluster['google_dataproc_cluster.default.name'] + + factory = test.replay_flight_data('dataproc-clusters-get', project_id=project_id) + p = test.load_policy({ + 'name': 'dataproc-get', + 'resource': 'gcp.dataproc-clusters', + }, session_factory=factory) + resource = p.resource_manager.get_resource({ + 'resourceName': f'projects/{project_id}/regions/{region}/clusters/{cluster_name}', + }) + assert resource['clusterName'] == cluster_name + assert resource['c7n:region']['name'] == region + + def test_data_proc_query(test): project_id = test.project_id test.set_regions('us-central1') diff --git a/tools/c7n_gcp/tests/test_kms.py b/tools/c7n_gcp/tests/test_kms.py index b3306b5e9c8..f0d438a8b03 100644 --- a/tools/c7n_gcp/tests/test_kms.py +++ b/tools/c7n_gcp/tests/test_kms.py @@ -1,5 +1,6 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from gcp_common import BaseTest, event_data from pytest_terraform import terraform @@ -261,6 +262,72 @@ def test_kms_cryptokey_label(test): ) test.assertEqual(result['labels']['env'], 'production') + def test_kms_cryptokey_update_rotation_period_requires_next_rotation_time(test): + with test.assertRaisesRegex(PolicyValidationError, "is a dependency of"): + test.load_policy( + { + 'name': 'kms-cryptokey-update-invalid', + 'resource': 'gcp.kms-cryptokey', + 'actions': [ + {'type': 'update', 'rotationPeriod': '7776000s'} + ], + } + ) + + def test_kms_cryptokey_update_version_template_requires_a_field(test): + with test.assertRaisesRegex(PolicyValidationError, 'versionTemplate'): + test.load_policy( + { + 'name': 'kms-cryptokey-update-invalid', + 'resource': 'gcp.kms-cryptokey', + 'actions': [ + {'type': 'update', 'versionTemplate': {}} + ], + } + ) + + def test_kms_cryptokey_update_access_justifications_requires_a_reason(test): + with test.assertRaisesRegex(PolicyValidationError, 'allowedAccessReasons'): + test.load_policy( + { + 'name': 'kms-cryptokey-update-invalid', + 'resource': 'gcp.kms-cryptokey', + 'actions': [ + { + 'type': 'update', + 'keyAccessJustificationsPolicy': { + 'allowedAccessReasons': [], + }, + } + ], + } + ) + + def test_kms_cryptokey_update_rotation_requires_encrypt_decrypt_purpose(test): + factory = test.replay_flight_data('kms-cryptokey-update') + p = test.load_policy( + { + 'name': 'kms-cryptokey-update-rotation', + 'resource': 'gcp.kms-cryptokey', + 'actions': [ + {'type': 'update', + 'rotationPeriod': '7776000s', + 'nextRotationTime': '2027-01-01T00:00:00Z'} + ], + }, + session_factory=factory, + ) + action = p.resource_manager.actions[0] + resource = { + 'name': ( + 'projects/cloud-custodian/locations/us-central1/keyRings/' + 'c7n-test-keyring-redfish/cryptoKeys/asymmetric-key' + ), + 'purpose': 'ASYMMETRIC_SIGN', + } + with test.assertRaisesRegex(PolicyExecutionError, 'ENCRYPT_DECRYPT'): + action.process([resource]) + class KmsCryptoKeyVersionTest(BaseTest): def test_kms_cryptokey_version_query(self): @@ -374,3 +441,35 @@ def test_kms_keyring_filter(test, kms_location): resources = policy.run() assert len(resources) == 1 assert resources[0]['name'] == 'projects/cloud-custodian/locations/us-west1' + + +@terraform('kms_cryptokey') +def test_kms_cryptokey_update(test, kms_cryptokey): + key_name = kms_cryptokey['google_kms_crypto_key.c7n_test_key.id'] + factory = test.replay_flight_data('kms-cryptokey-update') + policy = test.load_policy( + { + 'name': 'kms-cryptokey-update', + 'resource': 'gcp.kms-cryptokey', + 'query': [{'location': 'us-central1'}], + 'filters': [{ + 'type': 'value', + 'key': 'name', + 'value': key_name, + }], + 'actions': [ + {'type': 'update', + 'rotationPeriod': '7776000s', + 'nextRotationTime': '2027-01-01T00:00:00Z'} + ] + }, + session_factory=factory, + ) + resources = policy.run() + assert len(resources) == 1 + assert 'rotationPeriod' not in resources[0] + + client = policy.resource_manager.get_client() + result = client.execute_query('get', {'name': key_name}) + assert result['rotationPeriod'] == '7776000s' + assert result['nextRotationTime'] == '2027-01-01T00:00:00Z' diff --git a/tools/c7n_kube/c7n_kube/query.py b/tools/c7n_kube/c7n_kube/query.py index a1589cacbf0..3f0d1a34d48 100644 --- a/tools/c7n_kube/c7n_kube/query.py +++ b/tools/c7n_kube/c7n_kube/query.py @@ -52,7 +52,11 @@ def _invoke_client_enum(self, client, enum_op, params, path): f"be silently dropped" ) first = False - token = res.get("metadata", {}).get("_continue") + metadata = res.get("metadata") or {} + # .to_dict() renames the wire-format key "continue" (a reserved + # word) to "_continue"; a raw dict response that never passed + # through .to_dict() keeps the real wire key. Check both. + token = metadata.get("continue") or metadata.get("_continue") if not token: return items params["_continue"] = token diff --git a/tools/c7n_kube/c7n_kube/resources/custom_resource.py b/tools/c7n_kube/c7n_kube/resources/custom_resource.py new file mode 100644 index 00000000000..1801c2d700e --- /dev/null +++ b/tools/c7n_kube/c7n_kube/resources/custom_resource.py @@ -0,0 +1,132 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from c7n.utils import local_session + +from c7n_kube.provider import resources +from c7n_kube.query import QueryMeta, QueryResourceManager, ResourceQuery, TypeInfo + +log = logging.getLogger("custodian.k8s.custom_resource") + + +def tag_custom_resource_instance(raw_instance, *, group, version, plural, scope): + """Tag a raw CRD instance dict with the identity of the CRD it came from. + + `kind` is deliberately not injected: every CustomObjectsApi list item + already carries its own real `kind` on the wire, unlike + group/version/plural/scope. + """ + tagged = dict(raw_instance) + tagged["crd_group"] = group + tagged["crd_version"] = version + tagged["crd_plural"] = plural + tagged["crd_scope"] = scope + return tagged + + +@resources.register("custom-resource") +class CustomResource(QueryResourceManager, metaclass=QueryMeta): + """Discovers every installed CustomResourceDefinition (served, storage + version only -- see _discover_crds), then lists instances of each, + landing them all under this single resource type regardless of kind. + + `resources()` is fully overridden rather than relying on + QueryResourceManager's default, since there's no single static + group/version/plural for this type. `filter_resources()` is still + called explicitly below -- dropping it would silently no-op any + `filters:` block against this type. + """ + + class resource_type(TypeInfo): + # Resolves session.client("CustomObjects", "") -> CustomObjectsApi. Not + # this type's own group/version -- there are many, one per discovered + # CRD, decided at sync time. + group = "CustomObjects" + version = "" + canonical_group = "custom-resource" + + def resources(self, query=None): + session = local_session(self.session_factory) + instances = [] + for crd in self._discover_crds(session): + try: + instances.extend(self._list_instances(session, crd)) + except Exception: + log.exception( + "failed to list instances of CRD %s/%s %s (group=%s); " + "continuing with other discovered CRDs", + crd["plural"], + crd["version"], + crd["kind"], + crd["group"], + ) + return self.filter_resources(instances) + + def _discover_crds(self, session): + """List installed CustomResourceDefinitions, yielding only those + with a served, storage version. A CRD can serve multiple versions + simultaneously with potentially divergent schemas; instances are + listed under the storage version only, since it's the one + guaranteed to exist and be authoritative. + """ + client = session.client("Apiextensions", "V1") + for crd in client.list_custom_resource_definition().items: + storage_version = next((v for v in crd.spec.versions if v.storage), None) + if storage_version is None or not storage_version.served: + log.warning( + "skipping CRD %s/%s (group=%s): no served storage version -- " + "instances of this CRD will not appear until one exists", + crd.spec.names.plural, + crd.spec.names.kind, + crd.spec.group, + ) + continue + yield { + "kind": crd.spec.names.kind, + "group": crd.spec.group, + "version": storage_version.name, + "plural": crd.spec.names.plural, + "scope": crd.spec.scope, + } + + # Kubernetes list APIs only paginate when a caller opts in via `limit` -- + # a CRD like ArgoCD's Application can have thousands of instances, so + # pass a page size explicitly rather than making one large unpaginated + # call. + page_size = 250 + + def _list_instances(self, session, crd): + client = session.client("CustomObjects", "") + if crd["scope"] == "Cluster": + enum_op = "list_cluster_custom_object" + params = { + "group": crd["group"], + "version": crd["version"], + "plural": crd["plural"], + "limit": self.page_size, + } + else: + # list_namespaced_custom_object needs a specific namespace, + # which this account/cluster-wide sync doesn't have. + enum_op = "list_custom_object_for_all_namespaces" + params = { + "group": crd["group"], + "version": crd["version"], + "resource_plural": crd["plural"], + "limit": self.page_size, + } + raw_items = ResourceQuery(self.session_factory)._invoke_client_enum( + client, enum_op, params, "items" + ) + return [ + tag_custom_resource_instance( + item, + group=crd["group"], + version=crd["version"], + plural=crd["plural"], + scope=crd["scope"], + ) + for item in raw_items + ] diff --git a/tools/c7n_kube/c7n_kube/resources/resource_map.py b/tools/c7n_kube/c7n_kube/resources/resource_map.py index 487a20ed542..6998217a50a 100644 --- a/tools/c7n_kube/c7n_kube/resources/resource_map.py +++ b/tools/c7n_kube/c7n_kube/resources/resource_map.py @@ -4,6 +4,7 @@ "k8s.config-map": "c7n_kube.resources.core.configmap.ConfigMap", "k8s.custom-cluster-resource": "c7n_kube.resources.crd.CustomResourceDefinition", "k8s.custom-namespaced-resource": "c7n_kube.resources.crd.CustomNamespacedResourceDefinition", + "k8s.custom-resource": "c7n_kube.resources.custom_resource.CustomResource", "k8s.daemon-set": "c7n_kube.resources.apps.daemonset.DaemonSet", "k8s.deployment": "c7n_kube.resources.apps.deployment.Deployment", "k8s.namespace": "c7n_kube.resources.core.namespace.Namespace", diff --git a/tools/c7n_kube/pyproject.toml b/tools/c7n_kube/pyproject.toml index fb1dc4e3805..1487f5daf17 100644 --- a/tools/c7n_kube/pyproject.toml +++ b/tools/c7n_kube/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "kubernetes>=10.0.1", ] name = "c7n_kube" -version = "0.2.52a1+e4c099e03" +version = "0.2.52a1+bf0b50dbc" description = "Cloud Custodian - Kubernetes Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_kube/tests/data/flights/TestCustomResourceDiscovery.test_resources_discovers_and_tags_real_crd_instances.yaml b/tools/c7n_kube/tests/data/flights/TestCustomResourceDiscovery.test_resources_discovers_and_tags_real_crd_instances.yaml new file mode 100644 index 00000000000..c00b9fce0b7 --- /dev/null +++ b/tools/c7n_kube/tests/data/flights/TestCustomResourceDiscovery.test_resources_discovers_and_tags_real_crd_instances.yaml @@ -0,0 +1,434 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - OpenAPI-Generator/36.0.3/python + method: GET + uri: https://ghost/apis/apiextensions.k8s.io/v1/customresourcedefinitions + response: + body: + string: '{"kind":"CustomResourceDefinitionList","apiVersion":"apiextensions.k8s.io/v1","metadata":{"resourceVersion":"250"},"items":[{"metadata":{"name":"nodepools.karpenter.sh","uid":"878c3058-92f1-486e-9166-a5cb822add5d","resourceVersion":"78","generation":1,"creationTimestamp":"2026-09-03T21:04:26Z","annotations":{"controller-gen.kubebuilder.io/version":"v0.20.1","kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apiextensions.k8s.io/v1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"annotations\":{\"controller-gen.kubebuilder.io/version\":\"v0.20.1\"},\"name\":\"nodepools.karpenter.sh\"},\"spec\":{\"group\":\"karpenter.sh\",\"names\":{\"categories\":[\"karpenter\"],\"kind\":\"NodePool\",\"listKind\":\"NodePoolList\",\"plural\":\"nodepools\",\"singular\":\"nodepool\"},\"scope\":\"Cluster\",\"versions\":[{\"additionalPrinterColumns\":[{\"jsonPath\":\".spec.template.spec.nodeClassRef.name\",\"name\":\"NodeClass\",\"type\":\"string\"},{\"jsonPath\":\".status.nodes\",\"name\":\"Nodes\",\"type\":\"string\"},{\"jsonPath\":\".status.conditions[?(@.type==\\\"Ready\\\")].status\",\"name\":\"Ready\",\"type\":\"string\"},{\"jsonPath\":\".metadata.creationTimestamp\",\"name\":\"Age\",\"type\":\"date\"},{\"jsonPath\":\".spec.weight\",\"name\":\"Weight\",\"priority\":1,\"type\":\"integer\"},{\"jsonPath\":\".status.resources.cpu\",\"name\":\"CPU\",\"priority\":1,\"type\":\"string\"},{\"jsonPath\":\".status.resources.memory\",\"name\":\"Memory\",\"priority\":1,\"type\":\"string\"}],\"name\":\"v1\",\"schema\":{\"openAPIV3Schema\":{\"description\":\"NodePool + is the Schema for the NodePools API\",\"properties\":{\"apiVersion\":{\"description\":\"APIVersion + defines the versioned schema of this representation of an object.\\nServers + should convert recognized schemas to the latest internal value, and\\nmay + reject unrecognized values.\\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\",\"type\":\"string\"},\"kind\":{\"description\":\"Kind + is a string value representing the REST resource this object represents.\\nServers + may infer this from the endpoint the client submits requests to.\\nCannot + be updated.\\nIn CamelCase.\\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\",\"type\":\"string\"},\"metadata\":{\"type\":\"object\"},\"spec\":{\"description\":\"NodePoolSpec + is the top level nodepool specification. Nodepools\\nlaunch nodes in response + to pods that are unschedulable. A single nodepool\\nis capable of managing + a diverse set of nodes. Node properties are determined\\nfrom a combination + of nodepool and pod scheduling constraints.\",\"properties\":{\"disruption\":{\"default\":{\"consolidateAfter\":\"0s\"},\"description\":\"Disruption + contains the parameters that relate to Karpenter''s disruption logic\",\"properties\":{\"budgets\":{\"default\":[{\"nodes\":\"10%\"}],\"description\":\"Budgets + is a list of Budgets.\\nIf there are multiple active budgets, Karpenter uses\\nthe + most restrictive value. If left undefined,\\nthis will default to one budget + with a value to 10%.\",\"items\":{\"description\":\"Budget defines when Karpenter + will restrict the\\nnumber of Node Claims that can be terminating simultaneously.\",\"properties\":{\"duration\":{\"description\":\"Duration + determines how long a Budget is active since each Schedule hit.\\nOnly minutes + and hours are accepted, as cron does not work in seconds.\\nIf omitted, the + budget is always active.\\nThis is required if Schedule is set.\\nThis regex + has an optional 0s at the end since the duration.String() always adds\\na + 0s at the end.\",\"pattern\":\"^((([0-9]+(h|m))|([0-9]+h[0-9]+m))(0s)?)$\",\"type\":\"string\"},\"nodes\":{\"default\":\"10%\",\"description\":\"Nodes + dictates the maximum number of NodeClaims owned by this NodePool\\nthat can + be terminating at once. This is calculated by counting nodes that\\nhave a + deletion timestamp set, or are actively being deleted by Karpenter.\\nThis + field is required when specifying a budget.\\nThis cannot be of type intstr.IntOrString + since kubebuilder doesn''t support pattern\\nchecking for int nodes for IntOrString + nodes.\\nRef: https://github.com/kubernetes-sigs/controller-tools/blob/55efe4be40394a288216dab63156b0a64fb82929/pkg/crd/markers/validation.go#L379-L388\",\"pattern\":\"^((100|[0-9]{1,2})%|[0-9]+)$\",\"type\":\"string\"},\"reasons\":{\"description\":\"reasons + is a list of disruption methods that this budget applies to. If Reasons is + not set, this budget applies to all methods.\\nOtherwise, this will apply + to each reason defined.\\nallowed reasons are Underutilized, Empty, and Drifted.\",\"items\":{\"description\":\"DisruptionReason + defines valid reasons for disruption budgets.\",\"enum\":[\"Underutilized\",\"Empty\",\"Drifted\"],\"type\":\"string\"},\"maxItems\":50,\"type\":\"array\",\"x-kubernetes-list-type\":\"set\"},\"schedule\":{\"description\":\"Schedule + specifies when a budget begins being active, following\\nthe upstream cronjob + syntax. If omitted, the budget is always active.\\nTimezones are not supported.\\nThis + field is required if Duration is set.\",\"pattern\":\"^(@(annually|yearly|monthly|weekly|daily|midnight|hourly))|((.+)\\\\s(.+)\\\\s(.+)\\\\s(.+)\\\\s(.+))$\",\"type\":\"string\"}},\"required\":[\"nodes\"],\"type\":\"object\"},\"maxItems\":50,\"type\":\"array\",\"x-kubernetes-list-type\":\"atomic\",\"x-kubernetes-validations\":[{\"message\":\"''schedule'' + must be set with ''duration''\",\"rule\":\"self.all(x, has(x.schedule) == + has(x.duration))\"}]},\"consolidateAfter\":{\"description\":\"ConsolidateAfter + is the duration the controller will wait\\nbefore attempting to terminate + nodes that are underutilized.\\nRefer to ConsolidationPolicy for how underutilization + is considered.\\nWhen replicas is set, ConsolidateAfter is simply ignored\",\"pattern\":\"^(([0-9]+(s|m|h))+|Never)$\",\"type\":\"string\"},\"consolidationPolicy\":{\"default\":\"WhenEmptyOrUnderutilized\",\"description\":\"ConsolidationPolicy + describes which nodes Karpenter can disrupt through its consolidation\\nalgorithm. + This policy defaults to \\\"WhenEmptyOrUnderutilized\\\" if not specified.\\nValid + values: \\\"WhenEmpty\\\", \\\"WhenEmptyOrUnderutilized\\\", \\\"Balanced\\\".\\nWhen + replicas is set, ConsolidationPolicy is simply ignored.\",\"enum\":[\"WhenEmpty\",\"WhenEmptyOrUnderutilized\",\"Balanced\"],\"type\":\"string\"}},\"required\":[\"consolidateAfter\"],\"type\":\"object\"},\"limits\":{\"additionalProperties\":{\"anyOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}],\"pattern\":\"^(\\\\+|-)?(([0-9]+(\\\\.[0-9]*)?)|(\\\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\\\+|-)?(([0-9]+(\\\\.[0-9]*)?)|(\\\\.[0-9]+))))?$\",\"x-kubernetes-int-or-string\":true},\"description\":\"Limits + define a set of bounds for provisioning capacity.\\nLimits other than limits.nodes + is not supported when replicas is set.\",\"type\":\"object\"},\"replicas\":{\"description\":\"Replicas + is the desired number of nodes for the NodePool. When specified, the NodePool + will\\nmaintain this fixed number of replicas rather than scaling based on + pod demand.\\nWhen replicas is set:\\n - The following fields are ignored:\\n * + disruption.consolidationPolicy\\n * disruption.consolidateAfter\\n - + Only limits.nodes is supported; other resource limits (e.g., CPU, memory) + must not be specified.\\n - Weight is not supported.\\nNote: This field is + alpha.\",\"format\":\"int64\",\"minimum\":0,\"type\":\"integer\"},\"template\":{\"description\":\"Template + contains the template of possibilities for the provisioning logic to launch + a NodeClaim with.\\nNodeClaims launched from this NodePool will often be further + constrained than the template specifies.\",\"properties\":{\"metadata\":{\"properties\":{\"annotations\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"annotations + is an unstructured key value map stored with a resource that may be\\nset + by external tools to store and retrieve arbitrary metadata. They are not\\nqueryable + and should be preserved when modifying objects.\\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations\",\"type\":\"object\",\"x-kubernetes-map-type\":\"granular\"},\"labels\":{\"additionalProperties\":{\"maxLength\":63,\"pattern\":\"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$\",\"type\":\"string\"},\"description\":\"labels + is a map of string keys and values that can be used to organize and categorize\\n(scope + and select) objects. May match selectors of replication controllers\\nand + services.\\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\",\"maxProperties\":100,\"type\":\"object\",\"x-kubernetes-map-type\":\"atomic\",\"x-kubernetes-validations\":[{\"message\":\"label + domain \\\"karpenter.sh\\\" is restricted\",\"rule\":\"self.all(x, x in [\\\"karpenter.sh/capacity-type\\\", + \\\"karpenter.sh/nodepool\\\"] || !x.find(\\\"^([^/]+)\\\").endsWith(\\\"karpenter.sh\\\"))\"},{\"message\":\"label + \\\"karpenter.sh/nodepool\\\" is restricted\",\"rule\":\"self.all(x, x != + \\\"karpenter.sh/nodepool\\\")\"},{\"message\":\"label \\\"kubernetes.io/hostname\\\" + is restricted\",\"rule\":\"self.all(x, x != \\\"kubernetes.io/hostname\\\")\"}]}},\"type\":\"object\"},\"spec\":{\"description\":\"NodeClaimTemplateSpec + describes the desired state of the NodeClaim in the Nodepool\\nNodeClaimTemplateSpec + is used in the NodePool''s NodeClaimTemplate, with the resource requests omitted + since\\nusers are not able to set resource requests in the NodePool.\",\"properties\":{\"expireAfter\":{\"default\":\"720h\",\"description\":\"ExpireAfter + is the duration the controller will wait\\nbefore terminating a node, measured + from when the node is created. This\\nis useful to implement features like + eventually consistent node upgrade,\\nmemory leak protection, and disruption + testing.\",\"pattern\":\"^(([0-9]+(s|m|h))+|Never)$\",\"type\":\"string\"},\"nodeClassRef\":{\"description\":\"NodeClassRef + is a reference to an object that defines provider specific configuration\",\"properties\":{\"group\":{\"description\":\"API + version of the referent\",\"pattern\":\"^[^/]*$\",\"type\":\"string\",\"x-kubernetes-validations\":[{\"message\":\"group + may not be empty\",\"rule\":\"self != ''''\"}]},\"kind\":{\"description\":\"Kind + of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\\\"\",\"type\":\"string\",\"x-kubernetes-validations\":[{\"message\":\"kind + may not be empty\",\"rule\":\"self != ''''\"}]},\"name\":{\"description\":\"Name + of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names\",\"type\":\"string\",\"x-kubernetes-validations\":[{\"message\":\"name + may not be empty\",\"rule\":\"self != ''''\"}]}},\"required\":[\"group\",\"kind\",\"name\"],\"type\":\"object\",\"x-kubernetes-validations\":[{\"message\":\"nodeClassRef.group + is immutable\",\"rule\":\"self.group == oldSelf.group\"},{\"message\":\"nodeClassRef.kind + is immutable\",\"rule\":\"self.kind == oldSelf.kind\"}]},\"requirements\":{\"description\":\"Requirements + are layered with GetLabels and applied to every node.\",\"items\":{\"description\":\"A + node selector requirement is a selector that contains values, a key, an operator + that relates the key and values\\nand minValues that represent the requirement + to have at least that many values.\",\"properties\":{\"key\":{\"description\":\"The + label key that the selector applies to.\",\"maxLength\":316,\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$\",\"type\":\"string\",\"x-kubernetes-validations\":[{\"message\":\"label + domain \\\"karpenter.sh\\\" is restricted\",\"rule\":\"self in [\\\"karpenter.sh/capacity-type\\\", + \\\"karpenter.sh/nodepool\\\"] || !self.find(\\\"^([^/]+)\\\").endsWith(\\\"karpenter.sh\\\")\"},{\"message\":\"label + \\\"karpenter.sh/nodepool\\\" is restricted\",\"rule\":\"self != \\\"karpenter.sh/nodepool\\\"\"},{\"message\":\"label + \\\"kubernetes.io/hostname\\\" is restricted\",\"rule\":\"self != \\\"kubernetes.io/hostname\\\"\"}]},\"minValues\":{\"description\":\"This + field is ALPHA and can be dropped or replaced at any time\\nMinValues is the + minimum number of unique values required to define the flexibility of the + specific requirement.\",\"maximum\":50,\"minimum\":1,\"type\":\"integer\"},\"operator\":{\"description\":\"Represents + a key''s relationship to a set of values.\\nValid operators are In, NotIn, + Exists, DoesNotExist. Gt, Lt, Gte, and Lte.\",\"enum\":[\"Gte\",\"Lte\",\"In\",\"NotIn\",\"Exists\",\"DoesNotExist\",\"Gt\",\"Lt\"],\"type\":\"string\"},\"values\":{\"description\":\"An + array of string values. If the operator is In or NotIn,\\nthe values array + must be non-empty. If the operator is Exists or DoesNotExist,\\nthe values + array must be empty. If the operator is Gt, Lt, Gte, or Lte, the values\\narray + must have a single element, which will be interpreted as an integer.\\nThis + array is replaced during a strategic merge patch.\",\"items\":{\"type\":\"string\"},\"maxLength\":63,\"pattern\":\"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$\",\"type\":\"array\",\"x-kubernetes-list-type\":\"atomic\"}},\"required\":[\"key\",\"operator\"],\"type\":\"object\"},\"maxItems\":100,\"type\":\"array\",\"x-kubernetes-list-type\":\"atomic\",\"x-kubernetes-validations\":[{\"message\":\"requirements + with operator ''In'' must have a value defined\",\"rule\":\"self.all(x, x.operator + == ''In'' ? x.values.size() != 0 : true)\"},{\"message\":\"requirements operator + ''Gt'', ''Lt'', ''Gte'', or ''Lte'' must have a single positive integer value\",\"rule\":\"self.all(x, + (x.operator == ''Gt'' || x.operator == ''Lt'' || x.operator == ''Gte'' || + x.operator == ''Lte'') ? (x.values.size() == 1 \\u0026\\u0026 int(x.values[0]) + \\u003e= 0) : true)\"},{\"message\":\"requirements with ''minValues'' must + have at least that many values specified in the ''values'' field\",\"rule\":\"self.all(x, + (x.operator == ''In'' \\u0026\\u0026 has(x.minValues)) ? x.values.size() \\u003e= + x.minValues : true)\"}]},\"startupTaints\":{\"description\":\"startupTaints + are taints that are applied to nodes upon startup which are expected to be + removed automatically\\nwithin a short period of time, typically by a DaemonSet + that tolerates the taint. These are commonly used by\\ndaemonsets to allow + initialization and enforce startup ordering. StartupTaints are ignored for + provisioning\\npurposes in that pods are not required to tolerate a StartupTaint + in order to have nodes provisioned for them.\",\"items\":{\"description\":\"The + node this Taint is attached to has the \\\"effect\\\" on\\nany pod that does + not tolerate the Taint.\",\"properties\":{\"effect\":{\"description\":\"Required. + The effect of the taint on pods\\nthat do not tolerate the taint.\\nValid + effects are NoSchedule, PreferNoSchedule and NoExecute.\",\"enum\":[\"NoSchedule\",\"PreferNoSchedule\",\"NoExecute\"],\"type\":\"string\"},\"key\":{\"description\":\"Required. + The taint key to be applied to a node.\",\"minLength\":1,\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$\",\"type\":\"string\"},\"timeAdded\":{\"description\":\"TimeAdded + represents the time at which the taint was added.\",\"format\":\"date-time\",\"type\":\"string\"},\"value\":{\"description\":\"The + taint value corresponding to the taint key.\",\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$\",\"type\":\"string\"}},\"required\":[\"effect\",\"key\"],\"type\":\"object\"},\"type\":\"array\",\"x-kubernetes-list-type\":\"atomic\"},\"taints\":{\"description\":\"taints + will be applied to the NodeClaim''s node.\",\"items\":{\"description\":\"The + node this Taint is attached to has the \\\"effect\\\" on\\nany pod that does + not tolerate the Taint.\",\"properties\":{\"effect\":{\"description\":\"Required. + The effect of the taint on pods\\nthat do not tolerate the taint.\\nValid + effects are NoSchedule, PreferNoSchedule and NoExecute.\",\"enum\":[\"NoSchedule\",\"PreferNoSchedule\",\"NoExecute\"],\"type\":\"string\"},\"key\":{\"description\":\"Required. + The taint key to be applied to a node.\",\"minLength\":1,\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$\",\"type\":\"string\"},\"timeAdded\":{\"description\":\"TimeAdded + represents the time at which the taint was added.\",\"format\":\"date-time\",\"type\":\"string\"},\"value\":{\"description\":\"The + taint value corresponding to the taint key.\",\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$\",\"type\":\"string\"}},\"required\":[\"effect\",\"key\"],\"type\":\"object\"},\"type\":\"array\",\"x-kubernetes-list-type\":\"atomic\"},\"terminationGracePeriod\":{\"description\":\"TerminationGracePeriod + is the maximum duration the controller will wait before forcefully deleting + the pods on a node, measured from when deletion is first initiated.\\n\\nWarning: + this feature takes precedence over a Pod''s terminationGracePeriodSeconds + value, and bypasses any blocked PDBs or the karpenter.sh/do-not-disrupt annotation.\\n\\nThis + field is intended to be used by cluster administrators to enforce that nodes + can be cycled within a given time period.\\nWhen set, drifted nodes will begin + draining even if there are pods blocking eviction. Draining will respect PDBs + and the do-not-disrupt annotation until the TGP is reached.\\n\\nKarpenter + will preemptively delete pods so their terminationGracePeriodSeconds align + with the node''s terminationGracePeriod.\\nIf a pod would be terminated without + being granted its full terminationGracePeriodSeconds prior to the node timeout,\\nthat + pod will be deleted at T = node timeout - pod terminationGracePeriodSeconds.\\n\\nThe + feature can also be used to allow maximum time limits for long-running jobs + which can delay node termination with preStop hooks.\\nIf left undefined, + the controller will wait indefinitely for pods to be drained.\",\"pattern\":\"^([0-9]+(s|m|h))+$\",\"type\":\"string\"}},\"required\":[\"nodeClassRef\",\"requirements\"],\"type\":\"object\"}},\"required\":[\"spec\"],\"type\":\"object\"},\"weight\":{\"description\":\"Weight + is the priority given to the nodepool during scheduling. A higher\\nnumerical + weight indicates that this nodepool will be ordered\\nahead of other nodepools + with lower weights. A nodepool with no weight\\nwill be treated as if it is + a nodepool with a weight of 0.\\nWeight is not supported when replicas is + set.\",\"format\":\"int32\",\"maximum\":100,\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"template\"],\"type\":\"object\",\"x-kubernetes-validations\":[{\"message\":\"Cannot + transition NodePool between static (replicas set) and dynamic (replicas unset) + provisioning modes\",\"rule\":\"has(self.replicas) == has(oldSelf.replicas)\"},{\"message\":\"only + ''limits.nodes'' is supported on static NodePools\",\"rule\":\"!has(self.replicas) + || (!has(self.limits) || size(self.limits) == 0 || (size(self.limits) == 1 + \\u0026\\u0026 ''nodes'' in self.limits))\"},{\"message\":\"''weight'' is + not supported on static NodePools\",\"rule\":\"!has(self.replicas) || !has(self.weight)\"}]},\"status\":{\"description\":\"NodePoolStatus + defines the observed state of NodePool\",\"properties\":{\"conditions\":{\"description\":\"Conditions + contains signals for health and readiness\",\"items\":{\"description\":\"Condition + aliases the upstream type and adds additional helper methods\",\"properties\":{\"lastTransitionTime\":{\"description\":\"lastTransitionTime + is the last time the condition transitioned from one status to another.\\nThis + should be when the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable.\",\"format\":\"date-time\",\"type\":\"string\"},\"message\":{\"description\":\"message + is a human readable message indicating details about the transition.\\nThis + may be an empty string.\",\"maxLength\":32768,\"type\":\"string\"},\"observedGeneration\":{\"description\":\"observedGeneration + represents the .metadata.generation that the condition was set based upon.\\nFor + instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date\\nwith respect to the current state of + the instance.\",\"format\":\"int64\",\"minimum\":0,\"type\":\"integer\"},\"reason\":{\"description\":\"reason + contains a programmatic identifier indicating the reason for the condition''s + last transition.\\nProducers of specific condition types may define expected + values and meanings for this field,\\nand whether the values are considered + a guaranteed API.\\nThe value should be a CamelCase string.\\nThis field may + not be empty.\",\"maxLength\":1024,\"minLength\":1,\"pattern\":\"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\"type\":\"string\"},\"status\":{\"description\":\"status + of the condition, one of True, False, Unknown.\",\"enum\":[\"True\",\"False\",\"Unknown\"],\"type\":\"string\"},\"type\":{\"description\":\"type + of condition in CamelCase or in foo.example.com/CamelCase.\",\"maxLength\":316,\"pattern\":\"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\"type\":\"string\"}},\"required\":[\"lastTransitionTime\",\"message\",\"reason\",\"status\",\"type\"],\"type\":\"object\"},\"type\":\"array\",\"x-kubernetes-list-map-keys\":[\"type\"],\"x-kubernetes-list-type\":\"map\"},\"nodeClassObservedGeneration\":{\"description\":\"NodeClassObservedGeneration + represents the observed nodeClass generation for referenced nodeClass. If + this does not match\\nthe actual NodeClass Generation, NodeRegistrationHealthy + status condition on the NodePool will be reset\",\"format\":\"int64\",\"type\":\"integer\"},\"nodes\":{\"default\":0,\"description\":\"Nodes + is the count of nodes associated with this NodePool\",\"format\":\"int64\",\"type\":\"integer\"},\"resources\":{\"additionalProperties\":{\"anyOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}],\"pattern\":\"^(\\\\+|-)?(([0-9]+(\\\\.[0-9]*)?)|(\\\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\\\+|-)?(([0-9]+(\\\\.[0-9]*)?)|(\\\\.[0-9]+))))?$\",\"x-kubernetes-int-or-string\":true},\"description\":\"Resources + is the list of resources that have been provisioned.\",\"type\":\"object\"}},\"type\":\"object\"}},\"required\":[\"spec\"],\"type\":\"object\"}},\"served\":true,\"storage\":true,\"subresources\":{\"scale\":{\"specReplicasPath\":\".spec.replicas\",\"statusReplicasPath\":\".status.nodes\"},\"status\":{}}}]}}\n"},"managedFields":[{"manager":"kube-apiserver","operation":"Update","apiVersion":"apiextensions.k8s.io/v1","time":"2026-09-03T21:04:26Z","fieldsType":"FieldsV1","fieldsV1":{"f:status":{"f:acceptedNames":{"f:categories":{},"f:kind":{},"f:listKind":{},"f:plural":{},"f:singular":{}},"f:conditions":{"k:{\"type\":\"Established\"}":{".":{},"f:lastTransitionTime":{},"f:message":{},"f:reason":{},"f:status":{},"f:type":{}},"k:{\"type\":\"NamesAccepted\"}":{".":{},"f:lastTransitionTime":{},"f:message":{},"f:reason":{},"f:status":{},"f:type":{}}}}},"subresource":"status"},{"manager":"kubectl-client-side-apply","operation":"Update","apiVersion":"apiextensions.k8s.io/v1","time":"2026-09-03T21:04:26Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:controller-gen.kubebuilder.io/version":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}}},"f:spec":{"f:conversion":{".":{},"f:strategy":{}},"f:group":{},"f:names":{"f:categories":{},"f:kind":{},"f:listKind":{},"f:plural":{},"f:singular":{}},"f:scope":{},"f:versions":{}}}}]},"spec":{"group":"karpenter.sh","names":{"plural":"nodepools","singular":"nodepool","kind":"NodePool","listKind":"NodePoolList","categories":["karpenter"]},"scope":"Cluster","versions":[{"name":"v1","served":true,"storage":true,"schema":{"openAPIV3Schema":{"description":"NodePool + is the Schema for the NodePools API","type":"object","required":["spec"],"properties":{"apiVersion":{"description":"APIVersion + defines the versioned schema of this representation of an object.\nServers + should convert recognized schemas to the latest internal value, and\nmay reject + unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources","type":"string"},"kind":{"description":"Kind + is a string value representing the REST resource this object represents.\nServers + may infer this from the endpoint the client submits requests to.\nCannot be + updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds","type":"string"},"metadata":{"type":"object"},"spec":{"description":"NodePoolSpec + is the top level nodepool specification. Nodepools\nlaunch nodes in response + to pods that are unschedulable. A single nodepool\nis capable of managing + a diverse set of nodes. Node properties are determined\nfrom a combination + of nodepool and pod scheduling constraints.","type":"object","required":["template"],"properties":{"disruption":{"description":"Disruption + contains the parameters that relate to Karpenter''s disruption logic","type":"object","default":{"consolidateAfter":"0s"},"required":["consolidateAfter"],"properties":{"budgets":{"description":"Budgets + is a list of Budgets.\nIf there are multiple active budgets, Karpenter uses\nthe + most restrictive value. If left undefined,\nthis will default to one budget + with a value to 10%.","type":"array","default":[{"nodes":"10%"}],"maxItems":50,"items":{"description":"Budget + defines when Karpenter will restrict the\nnumber of Node Claims that can be + terminating simultaneously.","type":"object","required":["nodes"],"properties":{"duration":{"description":"Duration + determines how long a Budget is active since each Schedule hit.\nOnly minutes + and hours are accepted, as cron does not work in seconds.\nIf omitted, the + budget is always active.\nThis is required if Schedule is set.\nThis regex + has an optional 0s at the end since the duration.String() always adds\na 0s + at the end.","type":"string","pattern":"^((([0-9]+(h|m))|([0-9]+h[0-9]+m))(0s)?)$"},"nodes":{"description":"Nodes + dictates the maximum number of NodeClaims owned by this NodePool\nthat can + be terminating at once. This is calculated by counting nodes that\nhave a + deletion timestamp set, or are actively being deleted by Karpenter.\nThis + field is required when specifying a budget.\nThis cannot be of type intstr.IntOrString + since kubebuilder doesn''t support pattern\nchecking for int nodes for IntOrString + nodes.\nRef: https://github.com/kubernetes-sigs/controller-tools/blob/55efe4be40394a288216dab63156b0a64fb82929/pkg/crd/markers/validation.go#L379-L388","type":"string","default":"10%","pattern":"^((100|[0-9]{1,2})%|[0-9]+)$"},"reasons":{"description":"reasons + is a list of disruption methods that this budget applies to. If Reasons is + not set, this budget applies to all methods.\nOtherwise, this will apply to + each reason defined.\nallowed reasons are Underutilized, Empty, and Drifted.","type":"array","maxItems":50,"items":{"description":"DisruptionReason + defines valid reasons for disruption budgets.","type":"string","enum":["Underutilized","Empty","Drifted"]},"x-kubernetes-list-type":"set"},"schedule":{"description":"Schedule + specifies when a budget begins being active, following\nthe upstream cronjob + syntax. If omitted, the budget is always active.\nTimezones are not supported.\nThis + field is required if Duration is set.","type":"string","pattern":"^(@(annually|yearly|monthly|weekly|daily|midnight|hourly))|((.+)\\s(.+)\\s(.+)\\s(.+)\\s(.+))$"}}},"x-kubernetes-list-type":"atomic","x-kubernetes-validations":[{"rule":"self.all(x, + has(x.schedule) == has(x.duration))","message":"''schedule'' must be set with + ''duration''"}]},"consolidateAfter":{"description":"ConsolidateAfter is the + duration the controller will wait\nbefore attempting to terminate nodes that + are underutilized.\nRefer to ConsolidationPolicy for how underutilization + is considered.\nWhen replicas is set, ConsolidateAfter is simply ignored","type":"string","pattern":"^(([0-9]+(s|m|h))+|Never)$"},"consolidationPolicy":{"description":"ConsolidationPolicy + describes which nodes Karpenter can disrupt through its consolidation\nalgorithm. + This policy defaults to \"WhenEmptyOrUnderutilized\" if not specified.\nValid + values: \"WhenEmpty\", \"WhenEmptyOrUnderutilized\", \"Balanced\".\nWhen replicas + is set, ConsolidationPolicy is simply ignored.","type":"string","default":"WhenEmptyOrUnderutilized","enum":["WhenEmpty","WhenEmptyOrUnderutilized","Balanced"]}}},"limits":{"description":"Limits + define a set of bounds for provisioning capacity.\nLimits other than limits.nodes + is not supported when replicas is set.","type":"object","additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}},"replicas":{"description":"Replicas + is the desired number of nodes for the NodePool. When specified, the NodePool + will\nmaintain this fixed number of replicas rather than scaling based on + pod demand.\nWhen replicas is set:\n - The following fields are ignored:\n * + disruption.consolidationPolicy\n * disruption.consolidateAfter\n - Only + limits.nodes is supported; other resource limits (e.g., CPU, memory) must + not be specified.\n - Weight is not supported.\nNote: This field is alpha.","type":"integer","format":"int64","minimum":0},"template":{"description":"Template + contains the template of possibilities for the provisioning logic to launch + a NodeClaim with.\nNodeClaims launched from this NodePool will often be further + constrained than the template specifies.","type":"object","required":["spec"],"properties":{"metadata":{"type":"object","properties":{"annotations":{"description":"annotations + is an unstructured key value map stored with a resource that may be\nset by + external tools to store and retrieve arbitrary metadata. They are not\nqueryable + and should be preserved when modifying objects.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations","type":"object","additionalProperties":{"type":"string"},"x-kubernetes-map-type":"granular"},"labels":{"description":"labels + is a map of string keys and values that can be used to organize and categorize\n(scope + and select) objects. May match selectors of replication controllers\nand services.\nMore + info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels","type":"object","maxProperties":100,"additionalProperties":{"type":"string","maxLength":63,"pattern":"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"},"x-kubernetes-map-type":"atomic","x-kubernetes-validations":[{"rule":"self.all(x, + x in [\"karpenter.sh/capacity-type\", \"karpenter.sh/nodepool\"] || !x.find(\"^([^/]+)\").endsWith(\"karpenter.sh\"))","message":"label + domain \"karpenter.sh\" is restricted"},{"rule":"self.all(x, x != \"karpenter.sh/nodepool\")","message":"label + \"karpenter.sh/nodepool\" is restricted"},{"rule":"self.all(x, x != \"kubernetes.io/hostname\")","message":"label + \"kubernetes.io/hostname\" is restricted"}]}}},"spec":{"description":"NodeClaimTemplateSpec + describes the desired state of the NodeClaim in the Nodepool\nNodeClaimTemplateSpec + is used in the NodePool''s NodeClaimTemplate, with the resource requests omitted + since\nusers are not able to set resource requests in the NodePool.","type":"object","required":["nodeClassRef","requirements"],"properties":{"expireAfter":{"description":"ExpireAfter + is the duration the controller will wait\nbefore terminating a node, measured + from when the node is created. This\nis useful to implement features like + eventually consistent node upgrade,\nmemory leak protection, and disruption + testing.","type":"string","default":"720h","pattern":"^(([0-9]+(s|m|h))+|Never)$"},"nodeClassRef":{"description":"NodeClassRef + is a reference to an object that defines provider specific configuration","type":"object","required":["group","kind","name"],"properties":{"group":{"description":"API + version of the referent","type":"string","pattern":"^[^/]*$","x-kubernetes-validations":[{"rule":"self + != ''''","message":"group may not be empty"}]},"kind":{"description":"Kind + of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\"","type":"string","x-kubernetes-validations":[{"rule":"self + != ''''","message":"kind may not be empty"}]},"name":{"description":"Name + of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names","type":"string","x-kubernetes-validations":[{"rule":"self + != ''''","message":"name may not be empty"}]}},"x-kubernetes-validations":[{"rule":"self.group + == oldSelf.group","message":"nodeClassRef.group is immutable"},{"rule":"self.kind + == oldSelf.kind","message":"nodeClassRef.kind is immutable"}]},"requirements":{"description":"Requirements + are layered with GetLabels and applied to every node.","type":"array","maxItems":100,"items":{"description":"A + node selector requirement is a selector that contains values, a key, an operator + that relates the key and values\nand minValues that represent the requirement + to have at least that many values.","type":"object","required":["key","operator"],"properties":{"key":{"description":"The + label key that the selector applies to.","type":"string","maxLength":316,"pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$","x-kubernetes-validations":[{"rule":"self + in [\"karpenter.sh/capacity-type\", \"karpenter.sh/nodepool\"] || !self.find(\"^([^/]+)\").endsWith(\"karpenter.sh\")","message":"label + domain \"karpenter.sh\" is restricted"},{"rule":"self != \"karpenter.sh/nodepool\"","message":"label + \"karpenter.sh/nodepool\" is restricted"},{"rule":"self != \"kubernetes.io/hostname\"","message":"label + \"kubernetes.io/hostname\" is restricted"}]},"minValues":{"description":"This + field is ALPHA and can be dropped or replaced at any time\nMinValues is the + minimum number of unique values required to define the flexibility of the + specific requirement.","type":"integer","maximum":50,"minimum":1},"operator":{"description":"Represents + a key''s relationship to a set of values.\nValid operators are In, NotIn, + Exists, DoesNotExist. Gt, Lt, Gte, and Lte.","type":"string","enum":["Gte","Lte","In","NotIn","Exists","DoesNotExist","Gt","Lt"]},"values":{"description":"An + array of string values. If the operator is In or NotIn,\nthe values array + must be non-empty. If the operator is Exists or DoesNotExist,\nthe values + array must be empty. If the operator is Gt, Lt, Gte, or Lte, the values\narray + must have a single element, which will be interpreted as an integer.\nThis + array is replaced during a strategic merge patch.","type":"array","maxLength":63,"pattern":"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$","items":{"type":"string"},"x-kubernetes-list-type":"atomic"}}},"x-kubernetes-list-type":"atomic","x-kubernetes-validations":[{"rule":"self.all(x, + x.operator == ''In'' ? x.values.size() != 0 : true)","message":"requirements + with operator ''In'' must have a value defined"},{"rule":"self.all(x, (x.operator + == ''Gt'' || x.operator == ''Lt'' || x.operator == ''Gte'' || x.operator == + ''Lte'') ? (x.values.size() == 1 \u0026\u0026 int(x.values[0]) \u003e= 0) + : true)","message":"requirements operator ''Gt'', ''Lt'', ''Gte'', or ''Lte'' + must have a single positive integer value"},{"rule":"self.all(x, (x.operator + == ''In'' \u0026\u0026 has(x.minValues)) ? x.values.size() \u003e= x.minValues + : true)","message":"requirements with ''minValues'' must have at least that + many values specified in the ''values'' field"}]},"startupTaints":{"description":"startupTaints + are taints that are applied to nodes upon startup which are expected to be + removed automatically\nwithin a short period of time, typically by a DaemonSet + that tolerates the taint. These are commonly used by\ndaemonsets to allow + initialization and enforce startup ordering. StartupTaints are ignored for + provisioning\npurposes in that pods are not required to tolerate a StartupTaint + in order to have nodes provisioned for them.","type":"array","items":{"description":"The + node this Taint is attached to has the \"effect\" on\nany pod that does not + tolerate the Taint.","type":"object","required":["effect","key"],"properties":{"effect":{"description":"Required. + The effect of the taint on pods\nthat do not tolerate the taint.\nValid effects + are NoSchedule, PreferNoSchedule and NoExecute.","type":"string","enum":["NoSchedule","PreferNoSchedule","NoExecute"]},"key":{"description":"Required. + The taint key to be applied to a node.","type":"string","minLength":1,"pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"},"timeAdded":{"description":"TimeAdded + represents the time at which the taint was added.","type":"string","format":"date-time"},"value":{"description":"The + taint value corresponding to the taint key.","type":"string","pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"}}},"x-kubernetes-list-type":"atomic"},"taints":{"description":"taints + will be applied to the NodeClaim''s node.","type":"array","items":{"description":"The + node this Taint is attached to has the \"effect\" on\nany pod that does not + tolerate the Taint.","type":"object","required":["effect","key"],"properties":{"effect":{"description":"Required. + The effect of the taint on pods\nthat do not tolerate the taint.\nValid effects + are NoSchedule, PreferNoSchedule and NoExecute.","type":"string","enum":["NoSchedule","PreferNoSchedule","NoExecute"]},"key":{"description":"Required. + The taint key to be applied to a node.","type":"string","minLength":1,"pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"},"timeAdded":{"description":"TimeAdded + represents the time at which the taint was added.","type":"string","format":"date-time"},"value":{"description":"The + taint value corresponding to the taint key.","type":"string","pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\\/))?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"}}},"x-kubernetes-list-type":"atomic"},"terminationGracePeriod":{"description":"TerminationGracePeriod + is the maximum duration the controller will wait before forcefully deleting + the pods on a node, measured from when deletion is first initiated.\n\nWarning: + this feature takes precedence over a Pod''s terminationGracePeriodSeconds + value, and bypasses any blocked PDBs or the karpenter.sh/do-not-disrupt annotation.\n\nThis + field is intended to be used by cluster administrators to enforce that nodes + can be cycled within a given time period.\nWhen set, drifted nodes will begin + draining even if there are pods blocking eviction. Draining will respect PDBs + and the do-not-disrupt annotation until the TGP is reached.\n\nKarpenter will + preemptively delete pods so their terminationGracePeriodSeconds align with + the node''s terminationGracePeriod.\nIf a pod would be terminated without + being granted its full terminationGracePeriodSeconds prior to the node timeout,\nthat + pod will be deleted at T = node timeout - pod terminationGracePeriodSeconds.\n\nThe + feature can also be used to allow maximum time limits for long-running jobs + which can delay node termination with preStop hooks.\nIf left undefined, the + controller will wait indefinitely for pods to be drained.","type":"string","pattern":"^([0-9]+(s|m|h))+$"}}}}},"weight":{"description":"Weight + is the priority given to the nodepool during scheduling. A higher\nnumerical + weight indicates that this nodepool will be ordered\nahead of other nodepools + with lower weights. A nodepool with no weight\nwill be treated as if it is + a nodepool with a weight of 0.\nWeight is not supported when replicas is set.","type":"integer","format":"int32","maximum":100,"minimum":1}},"x-kubernetes-validations":[{"rule":"has(self.replicas) + == has(oldSelf.replicas)","message":"Cannot transition NodePool between static + (replicas set) and dynamic (replicas unset) provisioning modes"},{"rule":"!has(self.replicas) + || (!has(self.limits) || size(self.limits) == 0 || (size(self.limits) == 1 + \u0026\u0026 ''nodes'' in self.limits))","message":"only ''limits.nodes'' + is supported on static NodePools"},{"rule":"!has(self.replicas) || !has(self.weight)","message":"''weight'' + is not supported on static NodePools"}]},"status":{"description":"NodePoolStatus + defines the observed state of NodePool","type":"object","properties":{"conditions":{"description":"Conditions + contains signals for health and readiness","type":"array","items":{"description":"Condition + aliases the upstream type and adds additional helper methods","type":"object","required":["lastTransitionTime","message","reason","status","type"],"properties":{"lastTransitionTime":{"description":"lastTransitionTime + is the last time the condition transitioned from one status to another.\nThis + should be when the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable.","type":"string","format":"date-time"},"message":{"description":"message + is a human readable message indicating details about the transition.\nThis + may be an empty string.","type":"string","maxLength":32768},"observedGeneration":{"description":"observedGeneration + represents the .metadata.generation that the condition was set based upon.\nFor + instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date\nwith respect to the current state of the + instance.","type":"integer","format":"int64","minimum":0},"reason":{"description":"reason + contains a programmatic identifier indicating the reason for the condition''s + last transition.\nProducers of specific condition types may define expected + values and meanings for this field,\nand whether the values are considered + a guaranteed API.\nThe value should be a CamelCase string.\nThis field may + not be empty.","type":"string","maxLength":1024,"minLength":1,"pattern":"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$"},"status":{"description":"status + of the condition, one of True, False, Unknown.","type":"string","enum":["True","False","Unknown"]},"type":{"description":"type + of condition in CamelCase or in foo.example.com/CamelCase.","type":"string","maxLength":316,"pattern":"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$"}}},"x-kubernetes-list-map-keys":["type"],"x-kubernetes-list-type":"map"},"nodeClassObservedGeneration":{"description":"NodeClassObservedGeneration + represents the observed nodeClass generation for referenced nodeClass. If + this does not match\nthe actual NodeClass Generation, NodeRegistrationHealthy + status condition on the NodePool will be reset","type":"integer","format":"int64"},"nodes":{"description":"Nodes + is the count of nodes associated with this NodePool","type":"integer","format":"int64","default":0},"resources":{"description":"Resources + is the list of resources that have been provisioned.","type":"object","additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}}}}},"subresources":{"status":{},"scale":{"specReplicasPath":".spec.replicas","statusReplicasPath":".status.nodes"}},"additionalPrinterColumns":[{"name":"NodeClass","type":"string","jsonPath":".spec.template.spec.nodeClassRef.name"},{"name":"Nodes","type":"string","jsonPath":".status.nodes"},{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type==\"Ready\")].status"},{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"},{"name":"Weight","type":"integer","priority":1,"jsonPath":".spec.weight"},{"name":"CPU","type":"string","priority":1,"jsonPath":".status.resources.cpu"},{"name":"Memory","type":"string","priority":1,"jsonPath":".status.resources.memory"}]}],"conversion":{"strategy":"None"}},"status":{"conditions":[{"type":"NamesAccepted","status":"True","lastTransitionTime":"2026-09-03T21:04:26Z","reason":"NoConflicts","message":"no + conflicts found"},{"type":"Established","status":"True","lastTransitionTime":"2026-09-03T21:04:26Z","reason":"InitialNamesAccepted","message":"the + initial names have been accepted"}],"acceptedNames":{"plural":"nodepools","singular":"nodepool","kind":"NodePool","listKind":"NodePoolList","categories":["karpenter"]},"storedVersions":["v1"]}}]} + + ' + headers: + Audit-Id: + - f08c99e6-e890-4259-953e-b3c4b0c5b693 + Cache-Control: + - no-cache, private + Content-Type: + - application/json + Date: + - Thu, 03 Sep 2026 21:17:43 GMT + Transfer-Encoding: + - chunked + X-Kubernetes-Pf-Flowschema-Uid: + - 17d0245f-080e-4bb8-8d5d-c90323293b5e + X-Kubernetes-Pf-Prioritylevel-Uid: + - e2737129-286c-4dce-9b08-921b3883f78b + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - OpenAPI-Generator/36.0.3/python + method: GET + uri: https://ghost/apis/karpenter.sh/v1/nodepools?limit=250 + response: + body: + string: '{"apiVersion":"karpenter.sh/v1","items":[{"apiVersion":"karpenter.sh/v1","kind":"NodePool","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"karpenter.sh/v1\",\"kind\":\"NodePool\",\"metadata\":{\"annotations\":{\"kubernetes.io/description\":\"General + purpose NodePool for generic workloads\"},\"name\":\"general-purpose\"},\"spec\":{\"template\":{\"spec\":{\"nodeClassRef\":{\"group\":\"karpenter.k8s.aws\",\"kind\":\"EC2NodeClass\",\"name\":\"default\"},\"requirements\":[{\"key\":\"kubernetes.io/arch\",\"operator\":\"In\",\"values\":[\"amd64\"]},{\"key\":\"kubernetes.io/os\",\"operator\":\"In\",\"values\":[\"linux\"]},{\"key\":\"karpenter.sh/capacity-type\",\"operator\":\"In\",\"values\":[\"on-demand\"]},{\"key\":\"karpenter.k8s.aws/instance-category\",\"operator\":\"In\",\"values\":[\"c\",\"m\",\"r\"]},{\"key\":\"karpenter.k8s.aws/instance-generation\",\"operator\":\"Gt\",\"values\":[\"2\"]}]}}}}\n","kubernetes.io/description":"General + purpose NodePool for generic workloads"},"creationTimestamp":"2026-09-03T21:04:36Z","generation":1,"managedFields":[{"apiVersion":"karpenter.sh/v1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{},"f:kubernetes.io/description":{}}},"f:spec":{".":{},"f:disruption":{".":{},"f:budgets":{},"f:consolidateAfter":{},"f:consolidationPolicy":{}},"f:template":{".":{},"f:spec":{".":{},"f:expireAfter":{},"f:nodeClassRef":{".":{},"f:group":{},"f:kind":{},"f:name":{}},"f:requirements":{}}}}},"manager":"kubectl-client-side-apply","operation":"Update","time":"2026-09-03T21:04:36Z"}],"name":"general-purpose","resourceVersion":"81","uid":"78daf545-7fda-4e80-89c5-b05f3e75041a"},"spec":{"disruption":{"budgets":[{"nodes":"10%"}],"consolidateAfter":"0s","consolidationPolicy":"WhenEmptyOrUnderutilized"},"template":{"spec":{"expireAfter":"720h","nodeClassRef":{"group":"karpenter.k8s.aws","kind":"EC2NodeClass","name":"default"},"requirements":[{"key":"kubernetes.io/arch","operator":"In","values":["amd64"]},{"key":"kubernetes.io/os","operator":"In","values":["linux"]},{"key":"karpenter.sh/capacity-type","operator":"In","values":["on-demand"]},{"key":"karpenter.k8s.aws/instance-category","operator":"In","values":["c","m","r"]},{"key":"karpenter.k8s.aws/instance-generation","operator":"Gt","values":["2"]}]}}}}],"kind":"NodePoolList","metadata":{"continue":"","resourceVersion":"250"}} + + ' + headers: + Audit-Id: + - 943ac330-2566-492f-8587-d5ecf3ab7f30 + Cache-Control: + - no-cache, private + Content-Type: + - application/json + Date: + - Thu, 03 Sep 2026 21:17:43 GMT + Transfer-Encoding: + - chunked + X-Kubernetes-Pf-Flowschema-Uid: + - 17d0245f-080e-4bb8-8d5d-c90323293b5e + X-Kubernetes-Pf-Prioritylevel-Uid: + - e2737129-286c-4dce-9b08-921b3883f78b + status: + code: 200 + message: OK +version: 1 diff --git a/tools/c7n_kube/tests/test_custom_resource_discovery.py b/tools/c7n_kube/tests/test_custom_resource_discovery.py new file mode 100644 index 00000000000..f16d46185fa --- /dev/null +++ b/tools/c7n_kube/tests/test_custom_resource_discovery.py @@ -0,0 +1,258 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from unittest.mock import MagicMock, patch + +from common_kube import KubeTest + +from c7n_kube.resources.custom_resource import CustomResource, tag_custom_resource_instance + + +class TestTagCustomResourceInstance(KubeTest): + """Pure function -- no live cluster, no cassette needed.""" + + def test_injects_crd_identity_without_touching_wire_content(self): + raw = { + "apiVersion": "karpenter.sh/v1", + "kind": "NodePool", + "metadata": {"name": "general-purpose", "uid": "abc-123"}, + "spec": {"weight": 1}, + } + tagged = tag_custom_resource_instance( + raw, group="karpenter.sh", version="v1", plural="nodepools", scope="Cluster" + ) + # Real wire content untouched. + self.assertEqual(tagged["kind"], "NodePool") + self.assertEqual(tagged["metadata"], raw["metadata"]) + self.assertEqual(tagged["spec"], raw["spec"]) + # Injected identity fields present. + self.assertEqual(tagged["crd_group"], "karpenter.sh") + self.assertEqual(tagged["crd_version"], "v1") + self.assertEqual(tagged["crd_plural"], "nodepools") + self.assertEqual(tagged["crd_scope"], "Cluster") + + def test_does_not_mutate_the_input_dict(self): + raw = {"kind": "NodePool", "metadata": {}, "spec": {}} + tag_custom_resource_instance( + raw, group="karpenter.sh", version="v1", plural="nodepools", scope="Cluster" + ) + self.assertNotIn("crd_group", raw) + + +class TestCustomResourceDiscovery(KubeTest): + def test_resources_discovers_and_tags_real_crd_instances(self): + # Recorded against a real kube-apiserver with the real Karpenter + # NodePool CRD installed and one real instance created -- discovery + # (ApiextensionsV1Api.list_custom_resource_definition, a typed call) + # plus instance listing (list_cluster_custom_object, a raw-dict + # call), exactly the two-call sequence _discover_crds/_list_instances + # perform. + factory = self.replay_flight_data() + p = self.load_policy( + {"name": "cr", "resource": "k8s.custom-resource"}, + session_factory=factory, + ) + resources = p.resource_manager.resources() + self.assertEqual(len(resources), 1) + r = resources[0] + self.assertEqual(r["kind"], "NodePool") + self.assertEqual(r["crd_group"], "karpenter.sh") + self.assertEqual(r["crd_version"], "v1") + self.assertEqual(r["crd_plural"], "nodepools") + self.assertEqual(r["crd_scope"], "Cluster") + self.assertEqual(r["metadata"]["name"], "general-purpose") + # Real server-populated fields, not a submitted manifest. + self.assertTrue(r["metadata"]["uid"]) + self.assertTrue(r["metadata"]["resourceVersion"]) + + +class TestCustomResourceFilterResourcesPreserved(KubeTest): + """filter_resources() must run on the aggregated stream -- confirmed by + a policy filters: block actually excluding a matching instance, not + silently no-opping (the round-1 review finding this guards against).""" + + def test_filters_block_excludes_matching_instance(self): + real_manager = self._build_manager_with_filter() + real_manager._discover_crds = MagicMock( + return_value=[ + { + "kind": "NodePool", + "group": "karpenter.sh", + "version": "v1", + "plural": "nodepools", + "scope": "Cluster", + } + ] + ) + real_manager._list_instances = MagicMock( + return_value=[ + { + "kind": "NodePool", + "crd_group": "karpenter.sh", + "metadata": {"name": "keep-me"}, + }, + { + "kind": "NodePool", + "crd_group": "karpenter.sh", + "metadata": {"name": "exclude-me"}, + }, + ] + ) + resources = real_manager.resources() + names = [r["metadata"]["name"] for r in resources] + self.assertEqual(names, ["keep-me"]) + + def _build_manager_with_filter(self): + # No real network call is made in this test (both discovery and + # listing are mocked below), so no cassette/live session is needed -- + # a plain MagicMock session_factory is enough to construct the policy. + p = self.load_policy( + { + "name": "cr", + "resource": "k8s.custom-resource", + "filters": [ + {"type": "value", "key": "metadata.name", "value": "exclude-me", "op": "ne"} + ], + }, + session_factory=lambda: MagicMock(), + ) + return p.resource_manager + + +class TestDiscoverCrdsVersionSelection(KubeTest): + """_discover_crds must pick the storage version among multiple served + versions, and skip (with a log) a CRD whose storage version isn't + currently served -- the round-1 review finding this guards against.""" + + def _build_manager(self): + p = self.load_policy( + {"name": "cr", "resource": "k8s.custom-resource"}, + session_factory=lambda: MagicMock(), + ) + return p.resource_manager + + def _fake_crd_version(self, name, *, storage, served): + v = MagicMock() + v.name = name + v.storage = storage + v.served = served + return v + + def _fake_crd(self, *, kind, group, plural, scope, versions): + crd = MagicMock() + crd.spec.names.kind = kind + crd.spec.names.plural = plural + crd.spec.group = group + crd.spec.scope = scope + crd.spec.versions = versions + return crd + + def test_selects_storage_version_among_multiple_served_versions(self): + manager = self._build_manager() + session = MagicMock() + crd = self._fake_crd( + kind="Widget", + group="example.com", + plural="widgets", + scope="Cluster", + versions=[ + self._fake_crd_version("v1beta1", storage=False, served=True), + self._fake_crd_version("v1", storage=True, served=True), + ], + ) + session.client.return_value.list_custom_resource_definition.return_value.items = [crd] + discovered = list(manager._discover_crds(session)) + self.assertEqual(len(discovered), 1) + self.assertEqual(discovered[0]["version"], "v1") + + def test_skips_crd_whose_storage_version_is_not_served(self): + manager = self._build_manager() + session = MagicMock() + crd = self._fake_crd( + kind="Widget", + group="example.com", + plural="widgets", + scope="Cluster", + versions=[self._fake_crd_version("v1", storage=True, served=False)], + ) + session.client.return_value.list_custom_resource_definition.return_value.items = [crd] + with self.assertLogs("custodian.k8s.custom_resource", level="WARNING") as logs: + discovered = list(manager._discover_crds(session)) + self.assertEqual(discovered, []) + self.assertTrue(any("widgets" in msg for msg in logs.output)) + + +class TestListInstancesNamespacedPath(KubeTest): + """_list_instances must use the all-namespaces enum op (with + resource_plural, not plural) for a Namespaced-scope CRD, and must not + strip a raw instance's own real `kind` field along the way -- the + round-1 review finding this guards against (only the Cluster-scoped + path had cassette coverage before).""" + + def test_namespaced_crd_uses_correct_enum_op_and_preserves_kind(self): + manager = self.load_policy( + {"name": "cr", "resource": "k8s.custom-resource"}, + session_factory=lambda: MagicMock(), + ).resource_manager + crd = { + "kind": "Addon", + "group": "k3s.cattle.io", + "version": "v1", + "plural": "addons", + "scope": "Namespaced", + } + raw_item = {"kind": "Addon", "metadata": {"name": "ccm", "namespace": "kube-system"}} + with patch( + "c7n_kube.resources.custom_resource.ResourceQuery._invoke_client_enum", + return_value=[raw_item], + ) as mock_enum: + tagged = manager._list_instances(MagicMock(), crd) + enum_op, params = mock_enum.call_args.args[1], mock_enum.call_args.args[2] + self.assertEqual(enum_op, "list_custom_object_for_all_namespaces") + self.assertIn("resource_plural", params) + self.assertNotIn("plural", params) + self.assertEqual(tagged[0]["kind"], "Addon") + self.assertEqual(tagged[0]["crd_scope"], "Namespaced") + + +class TestCustomResourceCanonicalGroup(KubeTest): + def test_service_is_not_mislabeled_as_core(self): + self.assertEqual(CustomResource.resource_type.canonical_group, "custom-resource") + + +class TestCustomResourceOneCrdFailureIsolation(KubeTest): + def test_one_crd_listing_failure_does_not_abort_others(self): + # No real network call is made (both discovery and listing are + # mocked below), so a plain MagicMock session_factory is enough. + p = self.load_policy( + {"name": "cr", "resource": "k8s.custom-resource"}, + session_factory=lambda: MagicMock(), + ) + manager = p.resource_manager + manager._discover_crds = MagicMock( + return_value=[ + { + "kind": "Broken", + "group": "broken.example.com", + "version": "v1", + "plural": "brokens", + "scope": "Cluster", + }, + { + "kind": "Working", + "group": "working.example.com", + "version": "v1", + "plural": "workings", + "scope": "Cluster", + }, + ] + ) + + def fake_list_instances(session, crd): + if crd["kind"] == "Broken": + raise RuntimeError("simulated RBAC denial") + return [{"kind": "Working", "metadata": {"name": "still-here"}}] + + manager._list_instances = fake_list_instances + resources = manager.resources() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["metadata"]["name"], "still-here") diff --git a/tools/c7n_kube/tests/test_query_pagination.py b/tools/c7n_kube/tests/test_query_pagination.py index 47ccd6b021a..fbef4d35f88 100644 --- a/tools/c7n_kube/tests/test_query_pagination.py +++ b/tools/c7n_kube/tests/test_query_pagination.py @@ -22,6 +22,28 @@ def test_pagination_multi_page(self): resources = query.filter(manager, limit=2) self.assertEqual(len(resources), 6) + def test_pagination_raw_dict_uses_wire_format_continue_key(self): + # A raw dict response (e.g. list_cluster_custom_object, never passed + # through .to_dict()) carries the continuation token under the + # literal wire key "continue", not "_continue" (see query.py). + first_page = { + "metadata": {"continue": "token"}, + "items": [{"metadata": {"name": "a"}}], + } + second_page = { + "metadata": {}, + "items": [{"metadata": {"name": "b"}}], + } + + client = MagicMock() + client.list_cluster_custom_object.side_effect = [first_page, second_page] + + query = ResourceQuery(session_factory=None) + resources = query._invoke_client_enum( + client, "list_cluster_custom_object", {"limit": 2}, "items" + ) + self.assertEqual(len(resources), 2) + def test_pagination_shape_change_raises(self): first_page = { "metadata": {"_continue": "token"}, diff --git a/tools/c7n_left/c7n_left/providers/terraform/variables.py b/tools/c7n_left/c7n_left/providers/terraform/variables.py index 737e80f17e4..99150ec5d7d 100644 --- a/tools/c7n_left/c7n_left/providers/terraform/variables.py +++ b/tools/c7n_left/c7n_left/providers/terraform/variables.py @@ -14,6 +14,13 @@ from ...core import log from .graph import TerraformGraph +# python-hcl2 8.x changed its defaults (quoted string values, injected +# `__is_block__` markers). These options restore the pre-8.x output shape +# that the rest of this module expects from .tfvars parsing. +HCL2_SERIALIZATION_OPTIONS = hcl2.SerializationOptions( + strip_string_quotes=True, explicit_blocks=False, preserve_heredocs=False +) + class VariableResolver: """Handle variable value inputs. @@ -111,7 +118,9 @@ def get_uninitialized_var_files(self): try: f_vars = json.loads(contents) except json.JSONDecodeError: - f_vars = hcl2.loads(contents) + f_vars = hcl2.loads( + contents, serialization_options=HCL2_SERIALIZATION_OPTIONS + ) fpath = type == "user" and self.var_files[idx] or f if isinstance(fpath, Path): diff --git a/tools/c7n_left/pyproject.toml b/tools/c7n_left/pyproject.toml index 7d770f82af7..58d2c7035ac 100644 --- a/tools/c7n_left/pyproject.toml +++ b/tools/c7n_left/pyproject.toml @@ -7,12 +7,12 @@ requires-python = "<4.0.0,>=3.9.2" dependencies = [ "c7n", "click>=8.0", - "python-hcl2<5.0.0,>=4.3.2", + "python-hcl2>=8.1.0,<9.0.0", "rich<15.0,>=14.0", "tfparse<1.0,>=0.6", ] name = "c7n_left" -version = "0.3.39a1+e4c099e03" +version = "0.3.39a1+bf0b50dbc" description = "Custodian policies for IAAC definitions" readme = "README.md" classifiers = [ diff --git a/tools/c7n_mailer/pyproject.toml b/tools/c7n_mailer/pyproject.toml index cdad0209883..fa9db3f1726 100644 --- a/tools/c7n_mailer/pyproject.toml +++ b/tools/c7n_mailer/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "Jinja2<4.0,>=3.0", "boto3>=1.11.12", "datadog<1.0.0,>=0.34.0", - "importlib-metadata<9.0,>=8.0", + "importlib-metadata>=8.0,<10.0", "jsonpatch<2.0,>=1.25", "jsonpointer>=2.0", "jsonschema>=4.18", @@ -20,7 +20,7 @@ dependencies = [ "types-six<2.0.0,>=1.16.10", ] name = "c7n_mailer" -version = "0.6.52a1+e4c099e03" +version = "0.6.52a1+bf0b50dbc" description = "Cloud Custodian - Reference Mailer" readme = "README.md" classifiers = [ diff --git a/tools/c7n_oci/pyproject.toml b/tools/c7n_oci/pyproject.toml index 0f82ea87326..789ab5465fe 100644 --- a/tools/c7n_oci/pyproject.toml +++ b/tools/c7n_oci/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "pyopenssl>=24" ] name = "c7n_oci" -version = "0.1.27a1+e4c099e03" +version = "0.1.27a1+bf0b50dbc" description = "Cloud Custodian - OCI Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_openstack/pyproject.toml b/tools/c7n_openstack/pyproject.toml index 411f484d6da..0669c60b5f3 100644 --- a/tools/c7n_openstack/pyproject.toml +++ b/tools/c7n_openstack/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "openstacksdk==0.52.0", ] name = "c7n_openstack" -version = "0.1.43a1+e4c099e03" +version = "0.1.43a1+bf0b50dbc" description = "Cloud Custodian - OpenStack Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_org/pyproject.toml b/tools/c7n_org/pyproject.toml index 7a38056a451..0c5a2d0993a 100644 --- a/tools/c7n_org/pyproject.toml +++ b/tools/c7n_org/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "click>=8.0", ] name = "c7n_org" -version = "0.6.52a1+e4c099e03" +version = "0.6.52a1+bf0b50dbc" description = "Cloud Custodian - Parallel Execution" readme = "README.md" classifiers = [ diff --git a/tools/c7n_policystream/pyproject.toml b/tools/c7n_policystream/pyproject.toml index b39d4ac0a7b..0d9bcf256cf 100644 --- a/tools/c7n_policystream/pyproject.toml +++ b/tools/c7n_policystream/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_policystream" -version = "0.4.52a1+e4c099e03" +version = "0.4.52a1+bf0b50dbc" description = "Cloud Custodian - Git Commits as Logical Policy Changes" readme = "README.md" classifiers = [ @@ -32,7 +32,7 @@ c7n-policystream = "policystream:cli" [dependency-groups] dev = [ - "mock<5.0.0,>=4.0.2", + "mock>=4.0.2,<6.0.0", "pytest", ] diff --git a/tools/c7n_sphinxext/pyproject.toml b/tools/c7n_sphinxext/pyproject.toml index b6e1bcc741b..14853269f2d 100644 --- a/tools/c7n_sphinxext/pyproject.toml +++ b/tools/c7n_sphinxext/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "sphinx-rtd-theme>=1.0.0", ] name = "c7n_sphinxext" -version = "1.1.52a1+e4c099e03" +version = "1.1.52a1+bf0b50dbc" description = "Cloud Custodian - Sphinx Extensions" classifiers = [ "License :: OSI Approved :: Apache Software License", diff --git a/tools/c7n_tencentcloud/pyproject.toml b/tools/c7n_tencentcloud/pyproject.toml index e54c112642b..4d7b9d19f92 100644 --- a/tools/c7n_tencentcloud/pyproject.toml +++ b/tools/c7n_tencentcloud/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tencentcloud-sdk-python<4.0.0,>=3.0.783", ] name = "c7n_tencentcloud" -version = "0.1.35a1+e4c099e03" +version = "0.1.35a1+bf0b50dbc" description = "Cloud Custodian - Tencent Cloud Provider" readme = "readme.md" diff --git a/tools/c7n_terraform/c7n_terraform/parser.py b/tools/c7n_terraform/c7n_terraform/parser.py index 20147bee933..fb32af50405 100644 --- a/tools/c7n_terraform/c7n_terraform/parser.py +++ b/tools/c7n_terraform/c7n_terraform/parser.py @@ -14,6 +14,12 @@ TF_JSON_SUFFIX = ".tf.json" TF_HCL_SUFFIX = ".tf" +# python-hcl2 8.x changed its defaults (quoted labels/string values, injected +# `__is_block__` markers). These options restore the pre-8.x output shape that +# the rest of this module (and c7n_terraform's callers) expect. +HCL2_SERIALIZATION_OPTIONS = hcl2.SerializationOptions( + strip_string_quotes=True, explicit_blocks=False, preserve_heredocs=False) + class Block(dict): @@ -408,7 +414,8 @@ def __init__(self): def _parse_hcl_file(self, tf_file): with open(tf_file) as fp: - return self._parse_tf_data(hcl2.load(fp)) + return self._parse_tf_data( + hcl2.load(fp, serialization_options=HCL2_SERIALIZATION_OPTIONS)) def _parse_json_file(self, tf_file): with open(tf_file) as fp: diff --git a/tools/c7n_terraform/pyproject.toml b/tools/c7n_terraform/pyproject.toml index 4d1d03cc888..f6469c8478e 100644 --- a/tools/c7n_terraform/pyproject.toml +++ b/tools/c7n_terraform/pyproject.toml @@ -7,7 +7,7 @@ requires-python = "<4.0.0,>=3.9.2" dependencies = [ "c7n", "click<9.0,>=8.0", - "python-hcl2<5.0.0,>=4.3.2", + "python-hcl2<9.0.0,>=8.1.0", ] name = "c7n_terraform" version = "0.1.36" diff --git a/tools/dev/license-check.py b/tools/dev/license-check.py index d22d83f4bd2..6eed1a2ff87 100644 --- a/tools/dev/license-check.py +++ b/tools/dev/license-check.py @@ -53,6 +53,7 @@ 'uritemplate', # Dual license BSD 3-Clause OR Apache-2.0 # https://github.com/GrahamDumpleton/wrapt/issues/298 'wrapt', # BSD-2-Clause .. packaging update is missing license spdx header + 'regex', # Apache-2.0 AND CNRI-Python, because it derives from CPython's re module ) ) diff --git a/uv.lock b/uv.lock index f7a8f864af0..ff3bb60c991 100644 --- a/uv.lock +++ b/uv.lock @@ -319,15 +319,15 @@ wheels = [ [[package]] name = "azure-cosmos" -version = "3.2.0" +version = "4.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, - { name = "six" }, + { name = "azure-core" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/d3/f74bf55c48851944b726cb36883c68d3c4bb887fb2196f216ca543c691e1/azure-cosmos-3.2.0.tar.gz", hash = "sha256:4f77cc558fecffac04377ba758ac4e23f076dc1c54e2cf2515f85bc15cbde5c6", size = 154607, upload-time = "2020-07-08T18:40:27.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/8a/97b68e849fff7d2a76d85e51acdfbd13072266d581dcb7f6ad32bdddbd27/azure_cosmos-4.16.4.tar.gz", hash = "sha256:5a98d33ce6d539a926f47c67b14587fbb8ba4fd662751ff8b0ebe7f9d3765a2e", size = 2462258, upload-time = "2026-09-02T14:59:54.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/4f/23ffc8e870df94ea6def08121245301e763eaee7236fb8c3d02d5ff66687/azure_cosmos-3.2.0-py2.py3-none-any.whl", hash = "sha256:313e766bcf5a1779802c274dec94d5b9cc0e4d0d269489c56606fd2464070fad", size = 106591, upload-time = "2020-07-08T18:40:26.085Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cc/5f4b06f49ec0bf26e8a390fb55c67ceb67573e94e83493b6b96837b2ee98/azure_cosmos-4.16.4-py3-none-any.whl", hash = "sha256:89bf558203335efc39551dccef6782aa2f286d94cc87dd9bd98efc37d8c69303", size = 502464, upload-time = "2026-09-02T14:59:56.414Z" }, ] [[package]] @@ -416,21 +416,21 @@ wheels = [ [[package]] name = "azure-keyvault-certificates" -version = "4.11.1" +version = "4.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/59/8024c8b631baa2000bf0cb52f61688f9e9569646420a22da574b3949d4ad/azure_keyvault_certificates-4.11.1.tar.gz", hash = "sha256:734d9a65faa0f2bd45b9e162d317acadec71056934ebf4bbfe058b23d74bd073", size = 198750, upload-time = "2026-05-05T17:24:13.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/95/dd5212834585d0fd72c89118e86dfe1aa332f815ac27464196649e212438/azure_keyvault_certificates-4.11.2.tar.gz", hash = "sha256:1fbc1df1ae1b9b5b49a5b5915f826ec0b1662ecfee909cc757ad35208398bfbd", size = 198941, upload-time = "2026-08-26T16:07:23.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/27/e70bf2ad1008700736ace33a164629f5a46b62a15d3fbb5f44e00f3a2937/azure_keyvault_certificates-4.11.1-py3-none-any.whl", hash = "sha256:33dc110e4c265dec923f2c5490d30039d87b2c28a765f4f35459b0cc2c4cde9b", size = 162146, upload-time = "2026-05-05T17:24:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/fb/97/fddbf3b5d470fa628f2680807baedf754062828c33f7e6d5b4e07c8141b2/azure_keyvault_certificates-4.11.2-py3-none-any.whl", hash = "sha256:cc719633bf70c0bb9540762ce76c605d123c24907dfb14f450bf62cc5322acda", size = 162213, upload-time = "2026-08-26T16:07:25.111Z" }, ] [[package]] name = "azure-keyvault-keys" -version = "4.11.1" +version = "4.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -438,23 +438,23 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/03/5ce6db28b545427d4ab572f6a4ef2a727b6b4e7bf6941cedddf98822535b/azure_keyvault_keys-4.11.1.tar.gz", hash = "sha256:90caa3a7b2c8f6b53c247ec115cf1c1dad7f107cc3aa9f35aff4838bbce7e562", size = 260915, upload-time = "2026-05-19T20:01:08.041Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/97/2881414bdb1c9885c2e2195364b726d0ea3a2ef8a01c99e71238fc72b3b8/azure_keyvault_keys-4.11.2.tar.gz", hash = "sha256:8459c86542d103f8b67acb845fae5168029b6f7eb0fd7678ed8628c7b928addf", size = 261148, upload-time = "2026-08-26T16:05:33.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/3d/7bed91ae9268cf48124cf6990d8cd2c3daff7a2bb1e91a439d26ee90d705/azure_keyvault_keys-4.11.1-py3-none-any.whl", hash = "sha256:f46cdf6ee7a9baf27f70e6838327032886c8a087041dd56397773b1639da8fe2", size = 200651, upload-time = "2026-05-19T20:01:09.809Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/7dd5c7ee55b608fb54143253214391e58c7fe404b115b557226f69495c52/azure_keyvault_keys-4.11.2-py3-none-any.whl", hash = "sha256:aa5eecd5a5f868d619542c5528bc8edbc9bc71d7ce46721c366a9d3c7cac6759", size = 200715, upload-time = "2026-08-26T16:05:35.588Z" }, ] [[package]] name = "azure-keyvault-secrets" -version = "4.11.1" +version = "4.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/ee/c875370169d708fdfa2302078d8506cc8df40645f46c2b8cc7466d7cb1f6/azure_keyvault_secrets-4.11.1.tar.gz", hash = "sha256:23b11cbecf4525081121e68a82d7f3a4211556f6dbb4ddbb390a94db8b72c938", size = 115813, upload-time = "2026-08-12T16:40:52.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/d5/41043abed42ca3ab6930c52a25956a8eb468af2db0acb5ce446ff5437ddc/azure_keyvault_secrets-4.11.2.tar.gz", hash = "sha256:c0d86db01a8ae34d1db92ec6b9ef92d9a2aef7fbb1a67c0956513d46b85d7adb", size = 116119, upload-time = "2026-08-25T21:55:39.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/79/32aa77853e694711233a2f36af9ad0a7eaae2cc5e57804055192a3a0eac2/azure_keyvault_secrets-4.11.1-py3-none-any.whl", hash = "sha256:a5745d17d56061130cc56aa53c02c783e5d597031faf551b1873948461e8e700", size = 103733, upload-time = "2026-08-12T16:40:53.777Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/dbdaf5f11b5a410fd29be4ac544880ff970ac09de7274f141801c90e179e/azure_keyvault_secrets-4.11.2-py3-none-any.whl", hash = "sha256:7abdd4a830bd8f4998bea0edbe69b8a772d672c17eabfdf3f77d8e4ef29442e9", size = 103809, upload-time = "2026-08-25T21:55:41.372Z" }, ] [[package]] @@ -1328,7 +1328,7 @@ wheels = [ [[package]] name = "azure-storage-blob" -version = "12.30.0" +version = "12.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -1336,9 +1336,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/48/84a820d898267f662b5c06f7cd76fdb8a9e272b44aa9376cef3ec0f6a294/azure_storage_blob-12.30.0.tar.gz", hash = "sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be", size = 618229, upload-time = "2026-06-08T11:45:35.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/7e/834d7bfcf999ab89d1bd3a5d235ece6824686da2f3d315e2162c613fe43d/azure_storage_blob-12.30.1.tar.gz", hash = "sha256:7a24f978c51d56a0375beebffcbe8453e59ae390d2695705848edc75083e4184", size = 624787, upload-time = "2026-08-27T19:12:54.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/0b/e106f0fd7fa785867d9ffcc47dc9e6237c0e58f51058473b777487a98edc/azure_storage_blob-12.30.0-py3-none-any.whl", hash = "sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423", size = 435610, upload-time = "2026-06-08T11:45:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/f06915ccf78a6d965901aae093bd7e88e486c52e0773202656fb29c2304a/azure_storage_blob-12.30.1-py3-none-any.whl", hash = "sha256:7dc09c37f4f58508e20532b4b4c178f4763f41b01e0b9063835b994fd9d2a7b3", size = 438131, upload-time = "2026-08-27T19:12:56.796Z" }, ] [[package]] @@ -1476,30 +1476,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.78" +version = "1.43.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/55/e026c943f7f1ed6d2f5e6035713f21233bbe9ee975008662dc64ca0d4ced/boto3-1.43.78.tar.gz", hash = "sha256:2fa59116e298171ef59e7600a8be6c01177faef8af4b9a4314b7a57a04009ada", size = 112679, upload-time = "2026-08-21T19:37:36.84Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/26/48b3da85526a72a02df55e564481fc348e93699c15f0f502681b12ac2c8a/boto3-1.43.89.tar.gz", hash = "sha256:c28abbe472e9b7cad08807356311aeec51bde5218c18489da827045d2267bfd9", size = 112702, upload-time = "2026-09-04T19:24:57.143Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/55/288b1f0d987e15db0417f5ea43da564bfa042063f508c259eec893134628/boto3-1.43.78-py3-none-any.whl", hash = "sha256:893f06a171469618e17de78dc927aca6e74fcf45a70d2c5918e9ac9919e96cc9", size = 140028, upload-time = "2026-08-21T19:37:35.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/12/e1b5cb4a00a9bfd72cf2d3f982c5826757aacdfc90aa4bd61902dcc94856/boto3-1.43.89-py3-none-any.whl", hash = "sha256:fe4190afe63eb562b6ba6a3911cf4427473b35fa047adde093bf696d3ae09fc0", size = 140028, upload-time = "2026-09-04T19:24:55.929Z" }, ] [[package]] name = "botocore" -version = "1.43.78" +version = "1.43.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/71/490aaa384855bf3b69405ded52ae77a0e5f4eeb2165044fa65466c4d3a73/botocore-1.43.78.tar.gz", hash = "sha256:e8238d22c1e1342025d75d2e33d154a375e7caad0fc67f77d77faa2d82668b94", size = 15982895, upload-time = "2026-08-21T19:37:32.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/06/f63fb1befdf77af18539fb24ea01f2da0f13965ed5de091061708ac96416/botocore-1.43.89.tar.gz", hash = "sha256:f0574942970742657b0e0716cf08c2dfe6bef8e6de5fbb7081c3424e262b4cca", size = 16074206, upload-time = "2026-09-04T19:24:52.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5b/b7c5c22767c0e8eb4bb2bd0a972d77bd0be3dd9908e15d4487094a31fa4b/botocore-1.43.78-py3-none-any.whl", hash = "sha256:ddd020493235e264b3bd12606f239a3d3b2dd7cfb1d25a0691061183c290c228", size = 15677214, upload-time = "2026-08-21T19:37:28.894Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9d/96f9dee6d12eedf1c2b4264eefd59c7ac8cac10daadb9a7bfccc9ee881c6/botocore-1.43.89-py3-none-any.whl", hash = "sha256:d7211220c815427fe71225acc6909e4ab5dfab3b03770e72fd16cf9eb86b3d1a", size = 15768272, upload-time = "2026-09-04T19:24:49.769Z" }, ] [[package]] @@ -1516,7 +1516,7 @@ wheels = [ [[package]] name = "c7n" -version = "0.9.53a1+e4c099e03" +version = "0.9.53a1+bf0b50dbc" source = { editable = "." } dependencies = [ { name = "argcomplete" }, @@ -1605,7 +1605,7 @@ lint = [ [[package]] name = "c7n-awscc" -version = "0.1.38a1+e4c099e03" +version = "0.1.38a1+bf0b50dbc" source = { editable = "tools/c7n_awscc" } dependencies = [ { name = "boto3" }, @@ -1630,7 +1630,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-azure" -version = "0.7.52a1+e4c099e03" +version = "0.7.52a1+bf0b50dbc" source = { editable = "tools/c7n_azure" } dependencies = [ { name = "adal" }, @@ -1728,7 +1728,7 @@ requires-dist = [ { name = "adal", specifier = ">=1.2.6,<2.0.0" }, { name = "applicationinsights", specifier = ">=0.11.9,<1.0.0" }, { name = "apscheduler", specifier = ">=3.6.3,<4.0.0" }, - { name = "azure-cosmos", specifier = ">=3.1.2,<4.0.0" }, + { name = "azure-cosmos", specifier = ">=3.1.2,<5.0.0" }, { name = "azure-cosmosdb-table", specifier = ">=1.0.6,<2.0.0" }, { name = "azure-functions", specifier = ">=1.0.8,<2.0.0" }, { name = "azure-graphrbac", specifier = ">=0.61.1,<1.0.0" }, @@ -1817,7 +1817,7 @@ dev = [ [[package]] name = "c7n-gcp" -version = "0.4.52a1+e4c099e03" +version = "0.4.52a1+bf0b50dbc" source = { editable = "tools/c7n_gcp" } dependencies = [ { name = "c7n" }, @@ -1844,7 +1844,7 @@ requires-dist = [ { name = "google-auth", specifier = ">=2.1.0,<3.0.0" }, { name = "google-cloud-logging", specifier = ">=3.2,<4.0" }, { name = "google-cloud-monitoring", specifier = ">=2.5.0,<3.0.0" }, - { name = "google-cloud-storage", specifier = ">=2.7,<3.0" }, + { name = "google-cloud-storage", specifier = ">=2.7,<4.0" }, { name = "pyrate-limiter", specifier = ">=2.8.4,<3.0.0" }, { name = "retrying", specifier = ">=1.3.3,<2.0.0" }, ] @@ -1858,7 +1858,7 @@ dev = [ [[package]] name = "c7n-kube" -version = "0.2.52a1+e4c099e03" +version = "0.2.52a1+bf0b50dbc" source = { editable = "tools/c7n_kube" } dependencies = [ { name = "c7n" }, @@ -1887,7 +1887,7 @@ dev = [ [[package]] name = "c7n-left" -version = "0.3.39a1+e4c099e03" +version = "0.3.39a1+bf0b50dbc" source = { editable = "tools/c7n_left" } dependencies = [ { name = "c7n" }, @@ -1907,7 +1907,7 @@ dev = [ requires-dist = [ { name = "c7n", editable = "." }, { name = "click", specifier = ">=8.0" }, - { name = "python-hcl2", specifier = ">=4.3.2,<5.0.0" }, + { name = "python-hcl2", specifier = ">=8.1.0,<9.0.0" }, { name = "rich", specifier = ">=14.0,<15.0" }, { name = "tfparse", specifier = ">=0.6,<1.0" }, ] @@ -1920,7 +1920,7 @@ dev = [ [[package]] name = "c7n-mailer" -version = "0.6.52a1+e4c099e03" +version = "0.6.52a1+bf0b50dbc" source = { editable = "tools/c7n_mailer" } dependencies = [ { name = "boto3" }, @@ -1962,7 +1962,7 @@ requires-dist = [ { name = "c7n-gcp", marker = "extra == 'gcp'", editable = "tools/c7n_gcp" }, { name = "datadog", specifier = ">=0.34.0,<1.0.0" }, { name = "google-cloud-secret-manager", marker = "extra == 'gcp'", specifier = ">=2.8.0,<3.0.0" }, - { name = "importlib-metadata", specifier = ">=8.0,<9.0" }, + { name = "importlib-metadata", specifier = ">=8.0,<10.0" }, { name = "jinja2", specifier = ">=3.0,<4.0" }, { name = "jsonpatch", specifier = ">=1.25,<2.0" }, { name = "jsonpointer", specifier = ">=2.0" }, @@ -1986,7 +1986,7 @@ dev = [ [[package]] name = "c7n-oci" -version = "0.1.27a1+e4c099e03" +version = "0.1.27a1+bf0b50dbc" source = { editable = "tools/c7n_oci" } dependencies = [ { name = "c7n" }, @@ -2011,7 +2011,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-openstack" -version = "0.1.43a1+e4c099e03" +version = "0.1.43a1+bf0b50dbc" source = { editable = "tools/c7n_openstack" } dependencies = [ { name = "c7n" }, @@ -2038,7 +2038,7 @@ dev = [ [[package]] name = "c7n-org" -version = "0.6.52a1+e4c099e03" +version = "0.6.52a1+bf0b50dbc" source = { editable = "tools/c7n_org" } dependencies = [ { name = "c7n" }, @@ -2061,7 +2061,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-policystream" -version = "0.4.52a1+e4c099e03" +version = "0.4.52a1+bf0b50dbc" source = { editable = "tools/c7n_policystream" } dependencies = [ { name = "boto3" }, @@ -2091,13 +2091,13 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "mock", specifier = ">=4.0.2,<5.0.0" }, + { name = "mock", specifier = ">=4.0.2,<6.0.0" }, { name = "pytest" }, ] [[package]] name = "c7n-sphinxext" -version = "1.1.52a1+e4c099e03" +version = "1.1.52a1+bf0b50dbc" source = { editable = "tools/c7n_sphinxext" } dependencies = [ { name = "c7n" }, @@ -2124,7 +2124,7 @@ requires-dist = [ [[package]] name = "c7n-tencentcloud" -version = "0.1.35a1+e4c099e03" +version = "0.1.35a1+bf0b50dbc" source = { editable = "tools/c7n_tencentcloud" } dependencies = [ { name = "c7n" }, @@ -2448,14 +2448,11 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -2494,130 +2491,130 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" }, - { url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" }, - { url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" }, - { url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" }, - { url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" }, - { url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" }, - { url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" }, - { url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, - { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, - { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, - { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, - { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, - { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, - { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, - { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, - { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, - { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, - { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, - { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, - { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, - { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, - { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, - { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, - { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, - { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, - { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, - { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, - { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, - { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, - { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, - { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, - { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, - { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, - { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, - { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, - { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, - { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, - { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, - { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, - { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, - { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, - { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, - { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, - { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, - { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, - { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, - { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, - { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, - { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, - { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, - { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +version = "7.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/fc/fbd92ecbe5efbe68cf9e708d858570ebd33f0761600358948882f1a2a96b/coverage-7.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36aed4951aedf04cbe9465e76f8e71219980a52b73d07afe69746cba6ba7b97a", size = 222890, upload-time = "2026-08-28T21:50:37.361Z" }, + { url = "https://files.pythonhosted.org/packages/c6/54/16d2a7602ddf169353344e135541731cc24c7c3ef0001b0302f4d1a3de1e/coverage-7.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb953835dbfa6d641ac3943e0986bc680f8abbdc2985af15b46c54985347146a", size = 223415, upload-time = "2026-08-28T21:50:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/a1/15a36a42b35f6dd66214701c4f797856a9e42d12d85f441101eeb349404c/coverage-7.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97051c4903689b1afedc2a354d6118223051e03588078b53048603bda9014577", size = 250146, upload-time = "2026-08-28T21:50:42.509Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/677f6363054d2de71fd0ca2071a796e3cf7cf82f8046933b34f2f91eb031/coverage-7.16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:770d4244c423dcafb5c31db393f429fe952b1bba23bbff7cc3886f8133769ba5", size = 251977, upload-time = "2026-08-28T21:50:44.137Z" }, + { url = "https://files.pythonhosted.org/packages/40/fc/9be462bb9257d84e3cc7517dc118c364db0eabdd6cb42272bb8667abedce/coverage-7.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e7de0cb87960c6c9b5cad760068dab767b2b49a3b9376e1992c1e2691a015e", size = 253840, upload-time = "2026-08-28T21:50:45.822Z" }, + { url = "https://files.pythonhosted.org/packages/24/cd/dc003310b876c793c88f8dcf64ca52db244e4b6f96772251b1814fbb0653/coverage-7.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c2c45ee1853668f0ea1a0ddff396421c9dc5ad25a56bfb94a895970c2d8e7c2", size = 255756, upload-time = "2026-08-28T21:50:47.351Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f2/7a0a3c57e488b24d3ed560fc0e449c3e94fe8da0b59ef8dd00b2581c813a/coverage-7.16.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6b2b9599e7513b0a9c5bf0357f9f8deaa4c2c821025b0693d420e6602748981", size = 250811, upload-time = "2026-08-28T21:50:48.974Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1d/fd0cffd02a34eec7b92cfa4089a9d82e95390facebd56e5603de780b4727/coverage-7.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fde65e0ea945920265dfe4a2108fc45eee2e2ea3d9c3073af6373ff9836aa71", size = 251882, upload-time = "2026-08-28T21:50:50.516Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/82159b5ab545209764eb3eb0f06e315e9a2fd975a72bbf6da48d5deb6e76/coverage-7.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78103e79f9378cb0e43ddaa728629a373c070df903c5dfa98b63ba2cfb4e8c42", size = 249886, upload-time = "2026-08-28T21:50:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d3/91/ad689bb219fc78eaea8ff2b2212fa6202d4b716a65a533387183bb7a78ad/coverage-7.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e40e323711b485592354069b1c027ef879cc2d11657eac09a6e5ad0b49ab7406", size = 253698, upload-time = "2026-08-28T21:50:53.832Z" }, + { url = "https://files.pythonhosted.org/packages/4a/57/8eef40eb196d3fa1c0bbd99466119a40f70e5f2242eaa8f8852f98f4d985/coverage-7.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c94ef980f7b94d9dab9dac076d44ca706654cd51bad19734e029084adf528c8e", size = 250158, upload-time = "2026-08-28T21:50:55.64Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/0c0240eb6917c81ea21180bbc098bc01c624868bb917e9a10fffed23b970/coverage-7.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b37ad5cbb77776f446e1b55b461eec2eef5c3e7130c72dc0e1447c3a9da2d199", size = 250759, upload-time = "2026-08-28T21:50:57.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/6b4986aedfaa69a4607f24cf127527dbab4d57bfbeef4fffa126a5dbad6a/coverage-7.16.0-cp310-cp310-win32.whl", hash = "sha256:9421dde689e68d9fd2b6cd7d8c4498e79b5431467b6298517e3f3e60fdbe80a7", size = 224939, upload-time = "2026-08-28T21:50:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/90/b4/43cdf444ace1c30c15446b143fa79d4bdd756cfdfce4c63f10c7697ba957/coverage-7.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:81d63b68b26304e3668edb103311c17fe13c2ed1c7fe973309819f27bf61c5b8", size = 225568, upload-time = "2026-08-28T21:51:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/c76bf165ff01664ca8b1ca7f2b2b5f311353d3959dbac1187dd21c6cc7f8/coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d", size = 223019, upload-time = "2026-08-28T21:51:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/16/7d/a47cebf71cb789b6e25de07035d350bff110d02f9c28bf32f92b4c818874/coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db", size = 223524, upload-time = "2026-08-28T21:51:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/51/b3/42e46d7e247ba33758156a0cc88dc64715f7e7b04640fbe430c4da437ab1/coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3", size = 253934, upload-time = "2026-08-28T21:51:05.365Z" }, + { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4", size = 255846, upload-time = "2026-08-28T21:51:06.922Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/38e5d8cf45af5db7419e9580bba4017113f8f1e2697cb6c52213bf7e7e40/coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719", size = 257953, upload-time = "2026-08-28T21:51:08.51Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bb/2f44b99723d0306095dacdf90f994631e299ff8f087a384b42ecc2d1ccb9/coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3", size = 259915, upload-time = "2026-08-28T21:51:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7d/3f1c312944d88b2d3cae8af72007c15dcf5f92bda6da6d433c2d5f050ee7/coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a", size = 254028, upload-time = "2026-08-28T21:51:11.845Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f6/52a7e26baeeca7f3114b15da5e840bebcfe6491eb234f6922d33c79ee8fc/coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c", size = 255648, upload-time = "2026-08-28T21:51:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d1/0673e78d9ca29d56f663623791338647753c673f0bc964e860086da07bce/coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9", size = 253708, upload-time = "2026-08-28T21:51:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6c/23/b74c87828369059415b20884b6f48260f049bff750d6eb454be8554732ab/coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d", size = 257479, upload-time = "2026-08-28T21:51:16.988Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/09e172472c45a956e226dddf82d449f245764208b7cea47b32a73df955a3/coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6", size = 253428, upload-time = "2026-08-28T21:51:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/e378e4f7ffa290ea4775b34e319fa182640bba650a2c6781af791b66b79a/coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce", size = 254337, upload-time = "2026-08-28T21:51:20.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/9a6ca653d86e46c3383a905f726a28bcf7bb2528088794d30a53687b381c/coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce", size = 225103, upload-time = "2026-08-28T21:51:22.685Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/6d4627be89ac02f579d88806875a5d6e328c59d7d79c594643c7a4460ef6/coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a", size = 225577, upload-time = "2026-08-28T21:51:24.334Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3d/d7be38564d00a17775426685776b4bf18e8a6048a085eccf65d75eb0fa5a/coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7", size = 225126, upload-time = "2026-08-28T21:51:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, + { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, + { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, + { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, + { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, + { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, + { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, + { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, + { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, + { url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z" }, + { url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/318e4379106bc8047ba235e3732ddc87d1b393ac3db9776f5405ff14f322/coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024", size = 223257, upload-time = "2026-08-28T21:53:25.376Z" }, + { url = "https://files.pythonhosted.org/packages/81/4d/a5c54d9144e9db6505749758ba50a28be624148873751728a59cbb72d27a/coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c", size = 223596, upload-time = "2026-08-28T21:53:27.461Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/38e93a10899c9315964c0a4e729b3e5867f8f46e977808f9c6fbda52525a/coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633", size = 254699, upload-time = "2026-08-28T21:53:29.715Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/acddda030b4630f68167f3daa94b41d22071847822a70d8178d43dcf678e/coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b", size = 257614, upload-time = "2026-08-28T21:53:31.948Z" }, + { url = "https://files.pythonhosted.org/packages/15/7e/225b182497c1ce6d3f0d76a3074a4dbc9f272300e92bb100df53b03de0aa/coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f", size = 259236, upload-time = "2026-08-28T21:53:34.291Z" }, + { url = "https://files.pythonhosted.org/packages/2e/19/76641ddc50cb2410ebbd0ed7fe1052614d0e5612e802a2817521adb9febb/coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68", size = 261433, upload-time = "2026-08-28T21:53:36.401Z" }, + { url = "https://files.pythonhosted.org/packages/12/9e/5f89de8b7c2017f36b68b4e4a25940723a748b21474820bf61e8bce0891c/coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be", size = 255182, upload-time = "2026-08-28T21:53:38.496Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c1/ce94b2ec502e79775efb5efa22c741ebb0bd2be10bdd29650825ff57bdcb/coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a", size = 257329, upload-time = "2026-08-28T21:53:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/3f5374df3a6ca19ee5f98a6bd21dbb05f1e9d399bd9978e9821d260eab5e/coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae", size = 255210, upload-time = "2026-08-28T21:53:43.393Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b8/1bc5751496d0be6fd9dde8ca547d9a8a9f07847856aba3f3ae5ac594cd81/coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee", size = 259442, upload-time = "2026-08-28T21:53:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/dc/8aca78e47e1e6fcc761cd28a20daf4a84bd847a7369e2701a93ccfc3d1fd/coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f", size = 254618, upload-time = "2026-08-28T21:53:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/fd/787842cdf6ce16ac5c1bd8a26549bab3b3f27b02500075bc540dc7853bca/coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73", size = 256541, upload-time = "2026-08-28T21:53:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/8df302cbef373dd1f3401044cdb94dfc74517e5af2af27b4d0e721557e0e/coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889", size = 225429, upload-time = "2026-08-28T21:53:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/85/87/5bad7ac45f76b3728ca211028ee561c2ede3ba44da401129e28bb8737291/coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627", size = 225903, upload-time = "2026-08-28T21:53:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ea/67d84b11caf240f059ec313f616d82212df5004e8bc85802c1edfc50bb3d/coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9", size = 225334, upload-time = "2026-08-28T21:53:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/65/21/a88349cce3ff720729b754916ac47e2e3646a8137552e4fa7cdd5967cc7f/coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025", size = 223980, upload-time = "2026-08-28T21:54:00.082Z" }, + { url = "https://files.pythonhosted.org/packages/fd/02/4d54abf3e6a4d8b7675921b20e91163b1064a5a9dbefebb71c05065dd136/coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3", size = 224276, upload-time = "2026-08-28T21:54:02.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/10dbc96d95d20b9b041045d293480bd49e536180e93af62dd7662376284d/coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a", size = 265135, upload-time = "2026-08-28T21:54:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/6b326544afd1a8aef3a495bbae109a7ab5baf23e04a2741d8d64e2df2ba2/coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178", size = 268216, upload-time = "2026-08-28T21:54:06.97Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/1dc8265f3ed990690e24d5f31ff79bc9fb9b25d54f9f89bebad5a6a8b7a1/coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2", size = 270772, upload-time = "2026-08-28T21:54:09.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/a7/3a8463713a402b44044ec832f4a76e442ce4b3a207804303f4d1dc1a9bb4/coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0", size = 271752, upload-time = "2026-08-28T21:54:11.701Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/dd5e795cfbe1842f69899189089ae289a96d6a68de312960ea668542e33c/coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b", size = 265589, upload-time = "2026-08-28T21:54:14.12Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/fd90636cbd95cb018312f6ca1ca2bbd70fbe8e4ee6f3992fc36a4230364e/coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0", size = 268596, upload-time = "2026-08-28T21:54:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/ef2d59264f3b3b358cc5885ca375e6cdbda7c195e78304d5aae800a72d9d/coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e", size = 265072, upload-time = "2026-08-28T21:54:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5b/400891c364c0170408d172501b340b18611800f4c42d8fbb16f9f5497c24/coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54", size = 269768, upload-time = "2026-08-28T21:54:20.985Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/9792c80271df04d287d21ed5d662fd8fa58b1737888d817679b1ce5d2fab/coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb", size = 265211, upload-time = "2026-08-28T21:54:23.344Z" }, + { url = "https://files.pythonhosted.org/packages/81/67/5b8f827cfa6616e6bd7ba9397acfe7e3c4fd5b9fca4125511d5089f55d5a/coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897", size = 267170, upload-time = "2026-08-28T21:54:25.85Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/c135d2d2cb617d744bc3e13c922f2fae66964494176ddef225dc4656bd2c/coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743", size = 225731, upload-time = "2026-08-28T21:54:28.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/dc3d53eadf155916e183bf5dfacbfc4aa5bfb7f13b7da11c01caa7a05cbc/coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3", size = 226562, upload-time = "2026-08-28T21:54:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/2f/00/ac9da1a60a4e84c3ad0f7db4723fd327154a8f9add210c0dcd2db3ec5156/coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc", size = 225872, upload-time = "2026-08-28T21:54:32.806Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, ] [package.optional-dependencies] @@ -2709,59 +2706,59 @@ sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c [[package]] name = "cryptography" -version = "50.0.0" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] [[package]] @@ -3050,7 +3047,7 @@ grpc = [ [[package]] name = "google-api-python-client" -version = "2.199.0" +version = "2.200.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -3059,22 +3056,22 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/ff/c58d475046b552754a5ee24d98912506b07ea7ac7f0a434b327ad194ca32/google_api_python_client-2.199.0.tar.gz", hash = "sha256:8150816e22e01b36aa4b7523cdc1a2d2164e81c4de8a9b338785d7ecb4390ec2", size = 15394941, upload-time = "2026-08-20T21:38:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/e5/12024a0ae2fd39a54ff47a3868c47344ffff4ff1cd5edf4dff523c1a9fc1/google_api_python_client-2.200.0.tar.gz", hash = "sha256:82aa18b851328ea04867fd51c5a0c8da2e1b86ec45ce08487e902e7726d4ee50", size = 15488849, upload-time = "2026-09-01T00:04:25.545Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/42/c443badc33d972ee0e0adcd481c32a71f15d3e04c23a7a3d758e4018c633/google_api_python_client-2.199.0-py3-none-any.whl", hash = "sha256:1d2fa0e7f9d68f063b1a9ff7ed290d6e6c93176260487bf3a991e41534ca23a3", size = 15991965, upload-time = "2026-08-20T21:38:37.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/5242d5a8a6077ae73bd8a2bd7e3161f2aab476f0868fb6f85cf88b1c8835/google_api_python_client-2.200.0-py3-none-any.whl", hash = "sha256:c0c2c4072a110b7fcc30ac151744d4410cddaba99c53cb4e815463308952b638", size = 16082149, upload-time = "2026-09-01T00:04:22.839Z" }, ] [[package]] name = "google-auth" -version = "2.56.3" +version = "2.57.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, ] [[package]] @@ -3134,7 +3131,7 @@ wheels = [ [[package]] name = "google-cloud-logging" -version = "3.16.2" +version = "3.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -3148,9 +3145,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/65/d7dd485edea0ff3b403c680675042fd999dbe8ee6bc881d2248dfbc609ea/google_cloud_logging-3.16.2.tar.gz", hash = "sha256:d65044004d3a2ae6eced1f0118f0c44dc551872ddb837ba815f75d159ca148c8", size = 294046, upload-time = "2026-08-06T06:24:27.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/08/73f21dd94c7da7c2580dd179fdaebf72b74f2ea06d9812924dfd57d6d765/google_cloud_logging-3.16.3.tar.gz", hash = "sha256:5d7924bbb33c066bb43f70d33ba66749ff1c29b207de4c80b2bce1aacd53f676", size = 294772, upload-time = "2026-08-25T19:18:54.693Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/2f/a5cd299307ab1fa8fa67be540cdea5a377947120bb6a072bfdb52928ad73/google_cloud_logging-3.16.2-py3-none-any.whl", hash = "sha256:797d4d50dd1c0fea6a851a911bbaa092e3690b45786fd8ea438ef762eeeb8f35", size = 233852, upload-time = "2026-08-06T06:23:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0f/f741092958bbb063ccc8ee13e9d2db8c7abedf6208de10ae699ce3cf4bc7/google_cloud_logging-3.16.3-py3-none-any.whl", hash = "sha256:6275384f21d4b142d25731de3107de38408e807742290a9939e19ea68de8c283", size = 233851, upload-time = "2026-08-24T21:55:40.182Z" }, ] [[package]] @@ -3188,7 +3185,7 @@ wheels = [ [[package]] name = "google-cloud-storage" -version = "2.19.0" +version = "3.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -3198,9 +3195,9 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/76/4d965702e96bb67976e755bed9828fa50306dca003dbee08b67f41dd265e/google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2", size = 5535488, upload-time = "2024-12-05T01:35:06.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/73bb7512df1d1aad6ce3f9aed847cd40e0cd400ba4a85d86ab8eb412e9cc/google_cloud_storage-3.13.1.tar.gz", hash = "sha256:a80bf8cac2794808aa61c50c5f769ecbbe2d10331bacd0d69d30e59b14b346b2", size = 17341051, upload-time = "2026-08-06T06:24:42.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba", size = 131787, upload-time = "2024-12-05T01:35:04.736Z" }, + { url = "https://files.pythonhosted.org/packages/06/6f/d69f0e185e08ddb58c323a0a935af2b492907b5de362bc08933b0a3b5644/google_cloud_storage-3.13.1-py3-none-any.whl", hash = "sha256:98208de6c21e85cecd3eb44551894efff33d98365500e178867d4305854a770a", size = 341486, upload-time = "2026-08-06T06:23:36.548Z" }, ] [[package]] @@ -3400,14 +3397,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.9.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, ] [[package]] @@ -3843,11 +3840,11 @@ wheels = [ [[package]] name = "mock" -version = "4.0.3" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/be/3ea39a8fd4ed3f9a25aae18a1bff2df7a610bca93c8ede7475e32d8b73a0/mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc", size = 72316, upload-time = "2020-12-10T07:33:13.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/03/b7e605db4a57c0f6fba744b11ef3ddf4ddebcada35022927a2b5fc623fdf/mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", size = 28536, upload-time = "2020-12-10T07:33:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" }, ] [[package]] @@ -3861,7 +3858,7 @@ wheels = [ [[package]] name = "moto" -version = "5.2.2" +version = "5.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, @@ -3872,9 +3869,9 @@ dependencies = [ { name = "werkzeug" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/63/d944f387582cc53f53febbff2b3fa36a6d2ed7c1feef8990bf646cfa9cba/moto-5.2.2.tar.gz", hash = "sha256:aac8023a429e125e91c91f8f4730a67b54f518cda587352f7e67252fe3168f75", size = 8678761, upload-time = "2026-06-06T18:57:54.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/0f/1682c01ca0608c25526afb150246a3c9c1f609caccbd39758de4850e31bc/moto-5.2.3.tar.gz", hash = "sha256:a9e95c3218b6eda18e74571f1ced11cb1bc3151467d562c1da6037b9d19832cb", size = 8785514, upload-time = "2026-08-22T18:46:00.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/45/13cff46f4f617a6e97e1d497d75abd913e250bb4c823a4985668c6e593e4/moto-5.2.2-py3-none-any.whl", hash = "sha256:3817f1e39721ca833579b921e53e3b68547ace6a34d848c9486fbb5905808de9", size = 6698689, upload-time = "2026-06-06T18:57:51.435Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/ee7ace7a49bd525455497fa2ff198111877d6f93b908b657adeb2cf057a4/moto-5.2.3-py3-none-any.whl", hash = "sha256:5ec31b3cdbb383a19d46030157bd82aaaa7a9cc5f564200f995663d347bd4728", size = 6780972, upload-time = "2026-08-22T18:45:55.719Z" }, ] [[package]] @@ -4192,9 +4189,10 @@ wheels = [ [[package]] name = "oci" -version = "2.184.2" +version = "2.185.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiohttp" }, { name = "certifi" }, { name = "circuitbreaker" }, { name = "crc32c" }, @@ -4205,9 +4203,9 @@ dependencies = [ { name = "pytz" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/91/147428da81ab96b41a3943ac984b9e4e0cda527b71c4e61b83f45c604eee/oci-2.184.2.tar.gz", hash = "sha256:71e16eabcd4e66e9956d0cd084060c54b1e030567012d074ac0ba38526b7e4ed", size = 18021615, upload-time = "2026-08-18T04:15:18.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/47/9dbc5ac04d7f4230d49be16f4d724d44877834eefe77290e5215e17364ab/oci-2.185.1.tar.gz", hash = "sha256:b703be0a94da5a2b10158ced904d2eaa732c72f0a5d4582e476758fa4db1a8ea", size = 18139822, upload-time = "2026-09-01T05:11:14.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/6f/492f55aa51d208e7495eb6bd307a00f0561f9892b91f003106c2da4bbcdf/oci-2.184.2-py3-none-any.whl", hash = "sha256:5cd73d82493c7f5ba94039502927cf8d48d61ebb4d945ead4042513d3834a60f", size = 36631564, upload-time = "2026-08-18T04:15:11.143Z" }, + { url = "https://files.pythonhosted.org/packages/fc/70/03c081f033f8bb1e73ed3098fa3340a2aafb97a0c96841a27188c6cc7ada/oci-2.185.1-py3-none-any.whl", hash = "sha256:b3028254c45e149806566b40fe981c32834a45160aad95570b4c2f9be3873578", size = 36808289, upload-time = "2026-09-01T05:11:07.385Z" }, ] [[package]] @@ -5104,14 +5102,15 @@ wheels = [ [[package]] name = "python-hcl2" -version = "4.3.5" +version = "8.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lark" }, + { name = "regex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/b8/6d82f3ac6b14493e524df946365670235570a2074cb2621ea67caa4e8952/python-hcl2-4.3.5.tar.gz", hash = "sha256:71fbe48ee9a13335828f04adff2b267e06045c44c99c737b13d940aa1468d101", size = 19350, upload-time = "2024-08-06T08:23:55.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/3b/a66df999c382abf2d37f6c7b618a782958615acd41a53615f994dfd6611b/python_hcl2-8.1.4.tar.gz", hash = "sha256:b4145c930540e99e3e9b4f7de297775bdf856df29782a7abdb0916cbfcab7865", size = 119307, upload-time = "2026-09-08T08:26:26.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/5c/9a1fb7ebbd9d070ae6a491e7051ac9b56f71c4c59f930fb42a8ef6c7f832/python_hcl2-4.3.5-py3-none-any.whl", hash = "sha256:e33795eed675ae59545e725d1de0a8d78b4166b30b6e0bc7699365bd06334068", size = 11301, upload-time = "2024-08-06T08:23:54.684Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c7/9b8918ab4954ecad677b03f36f9121d5467a4285f41507fdc6789a33dc3c/python_hcl2-8.1.4-py3-none-any.whl", hash = "sha256:75738bd95717d692a683495babf4d00063f1b211e2eca04b156c6d79b9a0feae", size = 102957, upload-time = "2026-09-08T08:26:25.344Z" }, ] [[package]] @@ -5325,6 +5324,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "regex" +version = "2026.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/9b/ce31551f7191af763ea8414f58289d0ec18ed1d9a531fdebf12dfb249db4/regex-2026.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d416ed4058ea38f69884f3db523b0414c0094d262dde63694aa254e32a433e6", size = 493961, upload-time = "2026-09-01T00:49:33.851Z" }, + { url = "https://files.pythonhosted.org/packages/ef/62/fec41d97d45c0ff82b609012d69813500d9682d88577b70332ea36b131af/regex-2026.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:107319f437fc382e1e445d0abf8923db07f76e5fb3245ac5b09604e5dc8d4f63", size = 295226, upload-time = "2026-09-01T00:49:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/a35c61819ddd788bca7dc7a1be48a6bf5456044e3bb681553a15611879a3/regex-2026.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05e9f7d16b42686fb38b1702071a7359469ba89e9d516e2ba5228e077dcac524", size = 290643, upload-time = "2026-09-01T00:49:37.169Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6e/f105bdb0ec755f6a1e5f563f6afbcc22b34427c4ce11ab7f265d2df216cb/regex-2026.9.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39d8d5490ba4b8f26ef2aef72b775b1e7fc1a5ebaf28d11e637eb27b0b6a3048", size = 785257, upload-time = "2026-09-01T00:49:38.69Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/96e18e5b2a63f7aaf9ede0a1da656e5ebe795fbec035169a54aa021b89af/regex-2026.9.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4381151a29a7b9307842ff444609c4b9a402775fbe9afb3ec3e34ec0396bbae", size = 854330, upload-time = "2026-09-01T00:49:40.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/5e/d6515ad82fb43e3d740abf33db479dd47ce7928c14feb37281ed762571e6/regex-2026.9.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0efb99024ad5ba9198ffa816156b3319c3164b5a8d1e35940d92fdc9158b8d9f", size = 899547, upload-time = "2026-09-01T00:49:41.77Z" }, + { url = "https://files.pythonhosted.org/packages/99/d8/b4a74d131e33f800bd774b85ec2d269c9fb9bbc3f82a06c698bb7da6a5c9/regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ab7ad8b557d1f21e8b4a97a2c1a2ac82cf10c92d6a875c90f038ca9ed85dc5b", size = 793625, upload-time = "2026-09-01T00:49:44.126Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f3/9d21f325b5ff3a95cd79486e37d810f08b72ae26a2be5a795cbe7be3f70e/regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2a36e654181ecb996241bae256d28f26b03d019d6ef65c61dcf44396ab07c548", size = 786296, upload-time = "2026-09-01T00:49:45.78Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e3/bd865f20ce6ee293dbb4c102fe930f2a46ec09daa25ec0662e5af2c41060/regex-2026.9.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d36e9097a1bc6eebe8858216ccd2326f561662de4e50c27533452491c1cf1685", size = 768520, upload-time = "2026-09-01T00:49:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/41/bf/28c8ab87ac526784173d84a7b83fdaf8192037051660415b96f732bf2515/regex-2026.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c407afee6ea4caa313e815814e76d8447fcd32f81e3331d7937c2d33ab80c1b", size = 774325, upload-time = "2026-09-01T00:49:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f3/8c3704dc3a477d3974e4ddac1f8722a9da3847bf45474821ac3e06289b1f/regex-2026.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:82c27460ff2ea683159a204db2f9ce0f9549dec7070aa0809fbc36226bac1816", size = 848352, upload-time = "2026-09-01T00:49:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/39/c0/83457e7847eeda7d53bb4e8f6e2ceddb96002303d4b670f1cf87310cc029/regex-2026.9.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5e49096c90364317897b8d9fe97e2f86063dc0fc0bcd352017ea6fdd703ff25d", size = 758565, upload-time = "2026-09-01T00:49:51.241Z" }, + { url = "https://files.pythonhosted.org/packages/68/48/4b23395c8d9e9cd004e0785f270259c65a3185118a527ab4b97df842261f/regex-2026.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:11f9b104fa7b23f736b50fe1980a1422eb4641865e82614a25413deabccae575", size = 837119, upload-time = "2026-09-01T00:49:52.821Z" }, + { url = "https://files.pythonhosted.org/packages/e7/58/0912f7fc2220e69a05ea8f4bfd647d1c4017bf37c6d45f30e107367bdc30/regex-2026.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:92e06a5ecabd6e8352da58b9201ae241a1e7382c1419defa9d2f64ad4ea6abf2", size = 782371, upload-time = "2026-09-01T00:49:54.687Z" }, + { url = "https://files.pythonhosted.org/packages/e6/56/194a09928d12c86956996ce87408885588f58a88ada042594b3cb196f5f7/regex-2026.9.3-cp310-cp310-win32.whl", hash = "sha256:6ff5a98456194621f757d6226f88035c00ebbeab0a9095dc9b5a9ec5cc94b50b", size = 266768, upload-time = "2026-09-01T00:49:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/e59cb72afa57b509346d12e36bf62ecc4322ca5588494cd1b2aee1876247/regex-2026.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:14f8969a92ed847f78e8eed81c6a0384a9e8058a56eb659f83ddb59879c2e4e3", size = 277994, upload-time = "2026-09-01T00:49:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/85/3a/2d1253c32f2f5059398b943329b920748efa062c25b7bcafa9b9334c5931/regex-2026.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:32a32d2664e602b20e4b9c11234cb32f2e985323e453b2b396ea4ae686d82052", size = 276961, upload-time = "2026-09-01T00:49:58.907Z" }, + { url = "https://files.pythonhosted.org/packages/db/fc/b4ff5d9550796f9f955b6c2470f37e2b582ec944638f3004499929a69c96/regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65", size = 493967, upload-time = "2026-09-01T00:50:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/ab60a24883b810a4b29065540f56e6f72590bb97d704d7e0ee7bcd2ba98c/regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4", size = 295227, upload-time = "2026-09-01T00:50:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/e3bf61522cd82eda85a90dd8f69c839f0ce1da64dc6e87f803bb79267c59/regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e", size = 290639, upload-time = "2026-09-01T00:50:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/e8492b054c5c9fe09bc0aefdc308b8f845ec2094b1ae779accec8f14dedb/regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328", size = 791712, upload-time = "2026-09-01T00:50:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/429ee3479bb438f7b2b41147fa68d63a7ef9c7486fcd3960e04e61133cf3/regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71", size = 861711, upload-time = "2026-09-01T00:50:06.988Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/6ebf3cec538ed49d0a4cf5729129fe39580ddfa80c1c59c4274651762202/regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e", size = 905894, upload-time = "2026-09-01T00:50:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/a7bb1e4038a170714c824dc7c739f8b911cb1172f87ab950be39e0691f95/regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01", size = 801135, upload-time = "2026-09-01T00:50:10.308Z" }, + { url = "https://files.pythonhosted.org/packages/bd/47/1b318fb249d5fa3dcb0375ad284d34155f0da4a2fe92c8f01edef0cb0258/regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141", size = 773578, upload-time = "2026-09-01T00:50:12.125Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/63f2644a0a3e92623f83ccadd04873916f5b650e2028bc44ee9df104a839/regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc", size = 783915, upload-time = "2026-09-01T00:50:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/f078963d1dbd3632f6e25f7d972cc9a9332649ed239995bfeb403cf88a3b/regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b", size = 854924, upload-time = "2026-09-01T00:50:15.826Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/457ac94a2a2aaa2ab5c076324dfd7e38e76f36d4d78923c4ddb8c3d93d75/regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124", size = 763172, upload-time = "2026-09-01T00:50:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/db145a6ad935cdc6c694381543d53e6e7d7212287d4f8885f965e91ea4a4/regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f", size = 844983, upload-time = "2026-09-01T00:50:19.572Z" }, + { url = "https://files.pythonhosted.org/packages/76/00/100346c5cc2384a7ce0adfb7dbb11f4a865c76e3a920fe68542042da66ea/regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce", size = 789532, upload-time = "2026-09-01T00:50:21.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/d878c1d3eed2768f19e8b6fa4aa81b0a0e6bc23fa34e64173ef48816844a/regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420", size = 266766, upload-time = "2026-09-01T00:50:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/da/6a0ac27bacb39aca3452cdd7d1bfc361608ad000cea3e1b0d7dbaa1e2b15/regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc", size = 278007, upload-time = "2026-09-01T00:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a2/b23997814f48f823bcda615764b4fb00d72a5a06bdbb41cafa895ec62428/regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8", size = 276960, upload-time = "2026-09-01T00:50:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, + { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, + { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, + { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, + { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, + { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, + { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a0/322f050f77289a1acf99b5c400fe201780bdcfa8f8876fbe250ae3de37af/regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886", size = 496701, upload-time = "2026-09-01T00:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/91/30/85df182aec58aaa34b02a3f0a99068bf34a7c9b3f9bd7ff06d58aa866e1d/regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6", size = 297055, upload-time = "2026-09-01T00:51:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/64b8bbd0316baaede047dbdbf2bcc96f10a4eeddec2e9cbf289e855e636f/regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7", size = 292008, upload-time = "2026-09-01T00:51:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/224614ef16bc8336269a84b578fb0dbab1433b584aaefc5f0680cacf9227/regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944", size = 796341, upload-time = "2026-09-01T00:51:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e1/10ad988d3b673db86353666c56cb32e1b6246ca1ab0c81018a58ebec889c/regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b", size = 866311, upload-time = "2026-09-01T00:51:31.592Z" }, + { url = "https://files.pythonhosted.org/packages/08/16/8d597f97bb6215876512cd7b3c47179c0eb79d1820ab2ed8412d3ba5599a/regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8", size = 911869, upload-time = "2026-09-01T00:51:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/f6bcb26472873b9308df329f507d9c6cb526e3f9aed847498f153f2b7539/regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec", size = 801375, upload-time = "2026-09-01T00:51:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/38c7cd736bfd58b190412dbf5a81aae1c38456c538d5c4987f5980cbfa3d/regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69", size = 776522, upload-time = "2026-09-01T00:51:37.81Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/8ecefa1c9b57b29fcbe963797435e69a3ba6a9773d802b3d4fca4c75ecf6/regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7", size = 785727, upload-time = "2026-09-01T00:51:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4c/af402c5aa95615ab66620d775de07ea09b0fea206cd79e3c8a5716d5fc96/regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c", size = 861375, upload-time = "2026-09-01T00:51:41.829Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7b/b83dd26470fa4cc6d7beee9b3c52ff0e988ba636c1367a04d4b2829d8d95/regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc", size = 766221, upload-time = "2026-09-01T00:51:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/94/5b/3b91b19d8fb11a353738cec56b19440bf181f3f9a0298d91140fb4061778/regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69", size = 851737, upload-time = "2026-09-01T00:51:46.708Z" }, + { url = "https://files.pythonhosted.org/packages/2e/be/ea0762fd8a12fa711f1a626283f6d79a3d8a81df4eaada4fbe82009e46f6/regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380", size = 789455, upload-time = "2026-09-01T00:51:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dd/b9af9d65de78831fb76cb8ef420db4094295b09f93f8abd17c867261c4fb/regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd", size = 272528, upload-time = "2026-09-01T00:51:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fe/ecb15616ae7aa4892299b9ca7c20ef0dd6e5c833643b7ed46e27ff5fcccd/regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846", size = 280812, upload-time = "2026-09-01T00:51:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/18/4f/60efc748152c82b967bd186c356dd1fc21c4f6c225b8026b52547ae60c9f/regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16", size = 281095, upload-time = "2026-09-01T00:51:55.052Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cc/aa269583e8986be9604ef5fb9e2164f33108f6220c896efff2dfb0d4e4c4/regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16", size = 501014, upload-time = "2026-09-01T00:51:56.902Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/bb8a3f3a47beccbf92c3bcda67c2c0336c13c2e10f9744306275f428ebc9/regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d", size = 299376, upload-time = "2026-09-01T00:51:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/c646df2b62c1b1334e28581715ed776814c8ac80c0c9106175f0c61ce12a/regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595", size = 294424, upload-time = "2026-09-01T00:52:01.113Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/3dd8c49cc4af440a9a1a0ebb2b4a939398398c81fb7fecbc3c7d534924a1/regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17", size = 811421, upload-time = "2026-09-01T00:52:03.289Z" }, + { url = "https://files.pythonhosted.org/packages/71/32/c35f031aec040b9136189e3f403681e3168f1f9862e31d9df92ea599f2c1/regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970", size = 870110, upload-time = "2026-09-01T00:52:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7b/931d962f9fbca3326f282a01fa5bed4d6734dab5215d1c20bac0ab85df58/regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3", size = 917489, upload-time = "2026-09-01T00:52:07.703Z" }, + { url = "https://files.pythonhosted.org/packages/46/1a/da0c28f2b1e65d46a24abc1dd33e0dc9ddbcfeeee7c5625103b3e09e548c/regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5", size = 817470, upload-time = "2026-09-01T00:52:09.686Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/6a540ec40c3b8eeefec445362cf08bbb686b22d4088f97d3c1ea38323434/regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181", size = 784742, upload-time = "2026-09-01T00:52:11.778Z" }, + { url = "https://files.pythonhosted.org/packages/fc/88/75500ca27ba85256eb46119ebd99ec10ead4c3ffacd47fe7dfe034662960/regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef", size = 800609, upload-time = "2026-09-01T00:52:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/18541aaad8b88977e2c3fbbb53c1a75f6f6c999800a82d2f7306581cb6b6/regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae", size = 864798, upload-time = "2026-09-01T00:52:16.137Z" }, + { url = "https://files.pythonhosted.org/packages/16/62/ffbfbeb5a3f2cca701afb9f8aebbfcbf37a4aae2f1584c96680142aab445/regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88", size = 772892, upload-time = "2026-09-01T00:52:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/04e2db28f490b1fcc684428bd2de21e1ec46f7d8d421b7f2cc74d120affd/regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177", size = 858065, upload-time = "2026-09-01T00:52:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/c0cac930d1dc0b86c5d8ff10602f15df4fb15fa3f33e04a98be197f33618/regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e", size = 804739, upload-time = "2026-09-01T00:52:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/69e6f4d02e11a5cb45033abede6e04b0da3716b1b6c311d4609be14a01a7/regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f", size = 274514, upload-time = "2026-09-01T00:52:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/28/64/064b78028b52c0c21b9c0b362b1bdcee7179f009891ddf959d9b6390b30c/regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16", size = 283797, upload-time = "2026-09-01T00:52:27.358Z" }, + { url = "https://files.pythonhosted.org/packages/f2/46/fbb41eb6cc92403ba9f896dfd56a1d5d31549c4033dfb7418da7ecd5802a/regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5", size = 283444, upload-time = "2026-09-01T00:52:29.632Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1a/825beb1c803d579d1119723235ede2211949a8d320bcc79e3f6e5dd6090f/regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9", size = 496929, upload-time = "2026-09-01T00:52:31.593Z" }, + { url = "https://files.pythonhosted.org/packages/ff/54/9b8255521680d54e1d157ed6532e3082998f47925d2ab3861550e0e89455/regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361", size = 297050, upload-time = "2026-09-01T00:52:33.459Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0e4df28fd94304050efc2373dff1803e5d62056cf744a694eaa376df2fe8/regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab", size = 292296, upload-time = "2026-09-01T00:52:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/689a8403f309a7a3f3f3e74b3e81b144602481d122c9e4070a601367f8e0/regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5", size = 798376, upload-time = "2026-09-01T00:52:37.235Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a9/3d22278a38bb8adb254d345e60935316222091caf00a4233aaef388d636e/regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be", size = 866607, upload-time = "2026-09-01T00:52:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/69/ef/ecf8c5fdcec73ac0526fbd074be3bd1059b11cbda739ac0ab9001f311aed/regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e", size = 912160, upload-time = "2026-09-01T00:52:41.603Z" }, + { url = "https://files.pythonhosted.org/packages/68/e8/c07c3d2b44c9154672a33911cfcfba626c78d939d7e31103864d81ad319a/regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb", size = 804589, upload-time = "2026-09-01T00:52:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/d42957c78b5934efd03ca5713d99952cbe71e2d0b3b24d9e78256f208587/regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f", size = 784446, upload-time = "2026-09-01T00:52:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/4252dff33a1a5afb3271fb3859e992d34c47c040ecfc27bbfe6a7eaf99a8/regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c", size = 787996, upload-time = "2026-09-01T00:52:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/90d31048bb402fd746a95922d7dfdb9de9f902c39c8d838f2fef05c9c92c/regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27", size = 861752, upload-time = "2026-09-01T00:52:50.597Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/42ad133550cbd64d8fb33ec5ff55e1f524b06c6328e9f3bf9c3e8652b525/regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7", size = 773488, upload-time = "2026-09-01T00:52:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/78/8f/0c90e8fa607c36a65c360f6b02980aa7aa5885afa3d737daba4229bef086/regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253", size = 852603, upload-time = "2026-09-01T00:52:55.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bc/e1be309afb6be94916d8f4f000dbc66040fc0da2008b3f714dc4893259fd/regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20", size = 794635, upload-time = "2026-09-01T00:52:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/15f6d0ea137316531c992e7468a4c6f6e39159a6780c2c16ec3ab56f2149/regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae", size = 272530, upload-time = "2026-09-01T00:52:59.644Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/739168818790278dd4f1ba60c582fed41323508d7a8e850323851de1aa1f/regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57", size = 280822, upload-time = "2026-09-01T00:53:02.57Z" }, + { url = "https://files.pythonhosted.org/packages/12/6a/55f5f6e92a8c7d6511e069d3e8a4b45e3506941c1a97dddf5a329b132be4/regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950", size = 281102, upload-time = "2026-09-01T00:53:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/adaf9ed10ffdef60684bf8757c257400500342503f1ce5c7cffcdb2e8aaf/regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4", size = 501333, upload-time = "2026-09-01T00:53:06.681Z" }, + { url = "https://files.pythonhosted.org/packages/64/dd/2cbc3b253a88fdb0c8ca641367dc2f280541fdf1722f201fc0be62797577/regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff", size = 299273, upload-time = "2026-09-01T00:53:09.002Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/4818d31ec2555309b3935d3cd006a7f6076e11bb21eec9e72afee3db87b1/regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76", size = 294888, upload-time = "2026-09-01T00:53:11.009Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/8970a4102a067e0a28e40a232bf536752c373da8a4f932755679f77fa9ac/regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96", size = 813072, upload-time = "2026-09-01T00:53:13.566Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/2e52f5e9e92b79e324c9807e977fa6f9ab210b41ab5b7356307d5ffb2f4c/regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35", size = 869947, upload-time = "2026-09-01T00:53:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/c676e3f2dcb47ec941fa678019044cbac51db9441e09cec47392e4986013/regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa", size = 915678, upload-time = "2026-09-01T00:53:18.384Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/73c971bcaf1d24e73aa3c8e0f767caccfdc9fcda021e59f28f6dcd12728b/regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23", size = 817714, upload-time = "2026-09-01T00:53:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/0e9bffb6cfa8a00e765eaea0d1e6d207bb98e620d5658c537c8d87df671c/regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec", size = 792411, upload-time = "2026-09-01T00:53:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/61/d4/b1d226e8a3d0c8aac577a517384ffaea7b9ff98b194ddbd0bef58b9d8738/regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de", size = 802521, upload-time = "2026-09-01T00:53:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/dc47f94a4ace898561dfafeed6052dd3551915f36aa1971f4387d6daf0d4/regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f", size = 865266, upload-time = "2026-09-01T00:53:27.667Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/743565fc25cb36981186b4e9b42b3d093a7226cca328380cfd223c1b5a03/regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544", size = 780298, upload-time = "2026-09-01T00:53:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/d62e48324632eb0bae2c336da5810b51a14a9e23ba964f2828cc1ac23011/regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092", size = 858045, upload-time = "2026-09-01T00:53:32.397Z" }, + { url = "https://files.pythonhosted.org/packages/2e/41/8eff42a2516494bb6b36d33b15bcd861764dcc836ad7c0b746e46c513af9/regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69", size = 805406, upload-time = "2026-09-01T00:53:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1d/98198fd6dc185a172f5de48b8a4c7ca03c0d4285b1c1c32300672e7cf85a/regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a", size = 274674, upload-time = "2026-09-01T00:53:36.77Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/dc0e76842ccddc8756471adff38c140d130c6b601b9ae5724274f1881e7a/regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803", size = 283692, upload-time = "2026-09-01T00:53:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/da/3d/e0677f590f3473a3defb63ef6e3469154382a2a15ebafb364482d7ed3b01/regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2", size = 283421, upload-time = "2026-09-01T00:53:41.393Z" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -5687,27 +5823,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, - { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, - { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, - { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, - { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, - { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, - { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, - { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, +version = "0.16.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] @@ -5952,14 +6088,14 @@ wheels = [ [[package]] name = "tencentcloud-sdk-python" -version = "3.1.162" +version = "3.1.169" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/85/4de2797df7d6ed6cf1c50b267a5e28ed5b5101a9fa9e03b986b667c3489f/tencentcloud_sdk_python-3.1.162.tar.gz", hash = "sha256:479ba1c9aee234f8b23a7bfa3f6ecb81664f980e9de68a96a58040defc463909", size = 16500383, upload-time = "2026-08-21T08:30:55.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/25/0aa04c111e6a76f1737d269835ea67312e9c63fe399d73df698d54a4c9f4/tencentcloud_sdk_python-3.1.169.tar.gz", hash = "sha256:56ae413abf007c1b5a02059b8fbffe8249b815bc481fc2f8bce4b1633c104a5b", size = 16931418, upload-time = "2026-09-03T18:56:38.43Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/8c/88cd87f9eac191f5ecaa6b99841028fe50ecebee76a8912a59ebfdc72956/tencentcloud_sdk_python-3.1.162-py2.py3-none-any.whl", hash = "sha256:37bcca6e91d73ac95f85996b3e4940eea12c01d2e4cb016318974242f919f539", size = 17423436, upload-time = "2026-08-21T08:30:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/92/3b/90da2b98c73bccc27afb10699d50a07afacf6d7859b54a8d8add5d9cdfc8/tencentcloud_sdk_python-3.1.169-py2.py3-none-any.whl", hash = "sha256:58edafa0ad8f8883c91db4e13d35146ecb71a806d92017d46f5c91c573da554c", size = 17854559, upload-time = "2026-09-03T18:56:18.745Z" }, ] [[package]]