awspub 0.0.7__py3-none-any.whl → 0.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
awspub/common.py CHANGED
@@ -1,4 +1,10 @@
1
- from typing import Tuple
1
+ import logging
2
+ from typing import List, Tuple
3
+
4
+ import boto3
5
+ from mypy_boto3_ec2.client import EC2Client
6
+
7
+ logger = logging.getLogger(__name__)
2
8
 
3
9
 
4
10
  def _split_partition(val: str) -> Tuple[str, str]:
@@ -16,3 +22,43 @@ def _split_partition(val: str) -> Tuple[str, str]:
16
22
  partition = "aws"
17
23
  resource = val
18
24
  return partition, resource
25
+
26
+
27
+ def _get_regions(region_to_query: str, regions_allowlist: List[str]) -> List[str]:
28
+ """
29
+ Get a list of region names querying the `region_to_query` for all regions and
30
+ then filtering by `regions_allowlist`.
31
+ If no `regions_allowlist` is given, all queried regions are returned for the
32
+ current partition.
33
+ If `regions_allowlist` is given, all regions from that list are returned if
34
+ the listed region exist in the current partition.
35
+ Eg. `us-east-1` listed in `regions_allowlist` won't be returned if the current
36
+ partition is `aws-cn`.
37
+ :param region_to_query: region name of current partition
38
+ :type region_to_query: str
39
+ :praram regions_allowlist: list of regions in config file
40
+ :type regions_allowlist: List[str]
41
+ :return: list of regions names
42
+ :rtype: List[str]
43
+ """
44
+
45
+ # get all available regions
46
+ ec2client: EC2Client = boto3.client("ec2", region_name=region_to_query)
47
+ resp = ec2client.describe_regions()
48
+ ec2_regions_all = [r["RegionName"] for r in resp["Regions"]]
49
+
50
+ if regions_allowlist:
51
+ # filter out regions that are not available in the current partition
52
+ regions_allowlist_set = set(regions_allowlist)
53
+ ec2_regions_all_set = set(ec2_regions_all)
54
+ regions = list(regions_allowlist_set.intersection(ec2_regions_all_set))
55
+ diff = regions_allowlist_set.difference(ec2_regions_all_set)
56
+ if diff:
57
+ logger.warning(
58
+ f"regions {diff} listed in regions allowlist are not available in the current partition."
59
+ " Ignoring those."
60
+ )
61
+ else:
62
+ regions = ec2_regions_all
63
+
64
+ return regions
awspub/configmodels.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import pathlib
2
+ from enum import Enum
2
3
  from typing import Dict, List, Literal, Optional
3
4
 
4
5
  from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -94,6 +95,40 @@ class ConfigImageSSMParameterModel(BaseModel):
94
95
  )
95
96
 
96
97
 
98
+ class SNSNotificationProtocol(str, Enum):
99
+ DEFAULT = "default"
100
+ EMAIL = "email"
101
+
102
+
103
+ class ConfigImageSNSNotificationModel(BaseModel):
104
+ """
105
+ Image/AMI SNS Notification specific configuration to notify subscribers about new images availability
106
+ """
107
+
108
+ model_config = ConfigDict(extra="forbid")
109
+
110
+ subject: str = Field(description="The subject of SNS Notification", min_length=1, max_length=99)
111
+ message: Dict[SNSNotificationProtocol, str] = Field(
112
+ description="The body of the message to be sent to subscribers.",
113
+ default={SNSNotificationProtocol.DEFAULT: ""},
114
+ )
115
+ regions: Optional[List[str]] = Field(
116
+ description="Optional list of regions for sending notification. If not given, regions where the image "
117
+ "registered will be used from the currently used parition. If a region doesn't exist in the currently "
118
+ "used partition, it will be ignored.",
119
+ default=None,
120
+ )
121
+
122
+ @field_validator("message")
123
+ def check_message(cls, value):
124
+ # Check message protocols have default key
125
+ # Message should contain at least a top-level JSON key of “default”
126
+ # with a value that is a string
127
+ if SNSNotificationProtocol.DEFAULT not in value:
128
+ raise ValueError(f"{SNSNotificationProtocol.DEFAULT.value} key is required to send SNS notification")
129
+ return value
130
+
131
+
97
132
  class ConfigImageModel(BaseModel):
98
133
  """
99
134
  Image/AMI configuration.
@@ -148,6 +183,9 @@ class ConfigImageModel(BaseModel):
148
183
  )
149
184
  groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
150
185
  tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
186
+ sns: Optional[List[Dict[str, ConfigImageSNSNotificationModel]]] = Field(
187
+ description="Optional list of SNS Notification related configuration", default=None
188
+ )
151
189
 
152
190
  @field_validator("share")
153
191
  @classmethod
awspub/exceptions.py CHANGED
@@ -14,3 +14,11 @@ class BucketDoesNotExistException(Exception):
14
14
  def __init__(self, bucket_name: str, *args, **kwargs):
15
15
  msg = f"The bucket named '{bucket_name}' does not exist. You will need to create the bucket before proceeding."
16
16
  super().__init__(msg, *args, **kwargs)
17
+
18
+
19
+ class AWSNotificationException(Exception):
20
+ pass
21
+
22
+
23
+ class AWSAuthorizationException(Exception):
24
+ pass
awspub/image.py CHANGED
@@ -9,11 +9,12 @@ from mypy_boto3_ec2.client import EC2Client
9
9
  from mypy_boto3_ssm import SSMClient
10
10
 
11
11
  from awspub import exceptions
12
- from awspub.common import _split_partition
12
+ from awspub.common import _get_regions, _split_partition
13
13
  from awspub.context import Context
14
14
  from awspub.image_marketplace import ImageMarketplace
15
15
  from awspub.s3 import S3
16
16
  from awspub.snapshot import Snapshot
17
+ from awspub.sns import SNSNotification
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
@@ -120,28 +121,11 @@ class Image:
120
121
  @property
121
122
  def image_regions(self) -> List[str]:
122
123
  """
123
- Get the image regions. Either configured in the image configuration
124
- or all available regions.
125
- If a region is listed that is not available in the currently used partition,
126
- that region will be ignored (eg. having us-east-1 configured but running in the aws-cn
127
- partition doesn't include us-east-1 here).
124
+ Get the image regions.
128
125
  """
129
126
  if not self._image_regions_cached:
130
- # get all available regions
131
- ec2client: EC2Client = boto3.client("ec2", region_name=self._s3.bucket_region)
132
- resp = ec2client.describe_regions()
133
- image_regions_all = [r["RegionName"] for r in resp["Regions"]]
134
-
135
- if self.conf["regions"]:
136
- # filter out regions that are not available in the current partition
137
- image_regions_configured_set = set(self.conf["regions"])
138
- image_regions_all_set = set(image_regions_all)
139
- self._image_regions = list(image_regions_configured_set.intersection(image_regions_all_set))
140
- diff = image_regions_configured_set.difference(image_regions_all_set)
141
- if diff:
142
- logger.warning(f"configured regions {diff} not available in the current partition. Ignoring those.")
143
- else:
144
- self._image_regions = image_regions_all
127
+ regions_configured = self.conf["regions"] if "regions" in self.conf else []
128
+ self._image_regions = _get_regions(self._s3.bucket_region, regions_configured)
145
129
  self._image_regions_cached = True
146
130
  return self._image_regions
147
131
 
@@ -353,6 +337,13 @@ class Image:
353
337
  else:
354
338
  logger.error(f"image {self.image_name} not available in region {region}. can not make public")
355
339
 
340
+ def _sns_publish(self) -> None:
341
+ """
342
+ Publish SNS notifiations about newly available images to subscribers
343
+ """
344
+
345
+ SNSNotification(self._ctx, self.image_name).publish()
346
+
356
347
  def cleanup(self) -> None:
357
348
  """
358
349
  Cleanup/delete the temporary images
@@ -556,6 +547,10 @@ class Image:
556
547
  f"currently using partition {partition}. Ignoring marketplace config."
557
548
  )
558
549
 
550
+ # send ssn notification
551
+ if self.conf["sns"]:
552
+ self._sns_publish()
553
+
559
554
  def _verify(self, region: str) -> List[ImageVerificationErrors]:
560
555
  """
561
556
  Verify (but don't modify or create anything) the image in a single region
awspub/sns.py ADDED
@@ -0,0 +1,105 @@
1
+ """
2
+ Methods used to handle notifications for AWS using SNS
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Dict, List
8
+
9
+ import boto3
10
+ from botocore.exceptions import ClientError
11
+ from mypy_boto3_sns.client import SNSClient
12
+ from mypy_boto3_sts.client import STSClient
13
+
14
+ from awspub.common import _get_regions
15
+ from awspub.context import Context
16
+ from awspub.exceptions import AWSAuthorizationException, AWSNotificationException
17
+ from awspub.s3 import S3
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class SNSNotification(object):
23
+ """
24
+ A data object that contains validation logic and
25
+ structuring rules for SNS notification JSON
26
+ """
27
+
28
+ def __init__(self, context: Context, image_name: str):
29
+ """
30
+ Construct a message and verify that it is valid
31
+ """
32
+ self._ctx: Context = context
33
+ self._image_name: str = image_name
34
+ self._s3: S3 = S3(context)
35
+
36
+ @property
37
+ def conf(self) -> List[Dict[str, Any]]:
38
+ """
39
+ The sns configuration for the current image (based on "image_name") from context
40
+ """
41
+ return self._ctx.conf["images"][self._image_name]["sns"]
42
+
43
+ def _sns_regions(self, topic_config: Dict[Any, Any]) -> List[str]:
44
+ """
45
+ Get the sns regions. Either configured in the sns configuration
46
+ or all available regions.
47
+ If a region is listed that is not available in the currently used partition,
48
+ that region will be ignored (eg. having us-east-1 configured but running in the aws-cn
49
+ partition doesn't include us-east-1 here).
50
+ """
51
+
52
+ regions_configured = topic_config["regions"] if "regions" in topic_config else []
53
+ sns_regions = _get_regions(self._s3.bucket_region, regions_configured)
54
+
55
+ return sns_regions
56
+
57
+ def _get_topic_arn(self, topic_name: str, region_name: str) -> str:
58
+ """
59
+ Calculate topic ARN based on partition, region, account and topic name
60
+ :param topic_name: Name of topic
61
+ :type topic_name: str
62
+ :param region_name: name of region
63
+ :type region_name: str
64
+ :return: return topic ARN
65
+ :rtype: str
66
+ """
67
+
68
+ stsclient: STSClient = boto3.client("sts", region_name=region_name)
69
+ resp = stsclient.get_caller_identity()
70
+
71
+ account = resp["Account"]
72
+ # resp["Arn"] has string format "arn:partition:iam::accountnumber:user/iam_role"
73
+ partition = resp["Arn"].rsplit(":")[1]
74
+
75
+ return f"arn:{partition}:sns:{region_name}:{account}:{topic_name}"
76
+
77
+ def publish(self) -> None:
78
+ """
79
+ send notification to subscribers
80
+ """
81
+
82
+ for topic in self.conf:
83
+ for topic_name, topic_config in topic.items():
84
+ for region_name in self._sns_regions(topic_config):
85
+ snsclient: SNSClient = boto3.client("sns", region_name=region_name)
86
+ try:
87
+ snsclient.publish(
88
+ TopicArn=self._get_topic_arn(topic_name, region_name),
89
+ Subject=topic_config["subject"],
90
+ Message=json.dumps(topic_config["message"]),
91
+ MessageStructure="json",
92
+ )
93
+ except ClientError as e:
94
+ exception_code: str = e.response["Error"]["Code"]
95
+ if exception_code == "AuthorizationError":
96
+ raise AWSAuthorizationException(
97
+ "Profile does not have a permission to send the SNS notification."
98
+ " Please review the policy."
99
+ )
100
+ else:
101
+ raise AWSNotificationException(str(e))
102
+ logger.info(
103
+ f"The SNS notification {topic_config['subject']}"
104
+ f" for the topic {topic_name} in {region_name} has been sent."
105
+ )
@@ -118,6 +118,54 @@ awspub:
118
118
  -
119
119
  name: /awspub-test/param2
120
120
  allow_overwrite: true
121
+ "test-image-10":
122
+ boot_mode: "uefi"
123
+ description: |
124
+ A test image without a separate snapshot but single sns configs
125
+ regions:
126
+ - "us-east-1"
127
+ sns:
128
+ - "topic1":
129
+ subject: "topic1-subject"
130
+ message:
131
+ default: "default-message"
132
+ email: "email-message"
133
+ regions:
134
+ - "us-east-1"
135
+ "test-image-11":
136
+ boot_mode: "uefi"
137
+ description: |
138
+ A test image without a separate snapshot but multiple sns configs
139
+ regions:
140
+ - "us-east-1"
141
+ - "eu-central-1"
142
+ sns:
143
+ - "topic1":
144
+ subject: "topic1-subject"
145
+ message:
146
+ default: "default-message"
147
+ email: "email-message"
148
+ regions:
149
+ - "us-east-1"
150
+ - "topic2":
151
+ subject: "topic2-subject"
152
+ message:
153
+ default: "default-message"
154
+ regions:
155
+ - "us-gov-1"
156
+ - "eu-central-1"
157
+ "test-image-12":
158
+ boot_mode: "uefi"
159
+ description: |
160
+ A test image without a separate snapshot but single sns configs
161
+ regions:
162
+ - "us-east-1"
163
+ sns:
164
+ - "topic1":
165
+ subject: "topic1-subject"
166
+ message:
167
+ default: "default-message"
168
+ email: "email-message"
121
169
 
122
170
  tags:
123
171
  name: "foobar"
awspub/tests/test_api.py CHANGED
@@ -23,6 +23,9 @@ curdir = pathlib.Path(__file__).parent.resolve()
23
23
  "test-image-7",
24
24
  "test-image-8",
25
25
  "test-image-9",
26
+ "test-image-10",
27
+ "test-image-11",
28
+ "test-image-12",
26
29
  ],
27
30
  ),
28
31
  # with a group that no image as, no image should be processed
@@ -1,6 +1,8 @@
1
+ from unittest.mock import patch
2
+
1
3
  import pytest
2
4
 
3
- from awspub.common import _split_partition
5
+ from awspub.common import _get_regions, _split_partition
4
6
 
5
7
 
6
8
  @pytest.mark.parametrize(
@@ -14,3 +16,19 @@ from awspub.common import _split_partition
14
16
  )
15
17
  def test_common__split_partition(input, expected_output):
16
18
  assert _split_partition(input) == expected_output
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "regions_in_partition,configured_regions,expected_output",
23
+ [
24
+ (["region-1", "region-2"], ["region-1", "region-3"], ["region-1"]),
25
+ (["region-1", "region-2", "region-3"], ["region-4", "region-5"], []),
26
+ (["region-1", "region-2"], [], ["region-1", "region-2"]),
27
+ ],
28
+ )
29
+ def test_common__get_regions(regions_in_partition, configured_regions, expected_output):
30
+ with patch("boto3.client") as bclient_mock:
31
+ instance = bclient_mock.return_value
32
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
33
+
34
+ assert _get_regions("", configured_regions) == expected_output
@@ -127,16 +127,33 @@ def test_image___get_root_device_snapshot_id(root_device_name, block_device_mapp
127
127
 
128
128
 
129
129
  @pytest.mark.parametrize(
130
- "imagename,partition,called_mod_image,called_mod_snapshot,called_start_change_set,called_put_parameter",
130
+ (
131
+ "imagename",
132
+ "partition",
133
+ "called_mod_image",
134
+ "called_mod_snapshot",
135
+ "called_start_change_set",
136
+ "called_put_parameter",
137
+ "called_sns_publish",
138
+ ),
131
139
  [
132
- ("test-image-6", "aws", True, True, False, False),
133
- ("test-image-7", "aws", False, False, False, False),
134
- ("test-image-8", "aws", True, True, True, True),
135
- ("test-image-8", "aws-cn", True, True, False, True),
140
+ ("test-image-6", "aws", True, True, False, False, False),
141
+ ("test-image-7", "aws", False, False, False, False, False),
142
+ ("test-image-8", "aws", True, True, True, True, False),
143
+ ("test-image-8", "aws-cn", True, True, False, True, False),
144
+ ("test-image-10", "aws", False, False, False, False, True),
145
+ ("test-image-11", "aws", False, False, False, False, True),
146
+ ("test-image-12", "aws", False, False, False, False, True),
136
147
  ],
137
148
  )
138
149
  def test_image_publish(
139
- imagename, partition, called_mod_image, called_mod_snapshot, called_start_change_set, called_put_parameter
150
+ imagename,
151
+ partition,
152
+ called_mod_image,
153
+ called_mod_snapshot,
154
+ called_start_change_set,
155
+ called_put_parameter,
156
+ called_sns_publish,
140
157
  ):
141
158
  """
142
159
  Test the publish() for a given image
@@ -174,6 +191,7 @@ def test_image_publish(
174
191
  assert instance.modify_snapshot_attribute.called == called_mod_snapshot
175
192
  assert instance.start_change_set.called == called_start_change_set
176
193
  assert instance.put_parameter.called == called_put_parameter
194
+ assert instance.publish.called == called_sns_publish
177
195
 
178
196
 
179
197
  def test_image__get_zero_images():
@@ -0,0 +1,189 @@
1
+ import pathlib
2
+ from unittest.mock import patch
3
+
4
+ import botocore.exceptions
5
+ import pytest
6
+
7
+ from awspub import context, exceptions, sns
8
+
9
+ curdir = pathlib.Path(__file__).parent.resolve()
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ "imagename,called_sns_publish, publish_call_count",
14
+ [
15
+ ("test-image-10", True, 1),
16
+ ("test-image-11", True, 2),
17
+ ("test-image-12", True, 2),
18
+ ],
19
+ )
20
+ def test_sns_publish(imagename, called_sns_publish, publish_call_count):
21
+ """
22
+ Test the send_notification logic
23
+ """
24
+ with patch("boto3.client") as bclient_mock:
25
+ instance = bclient_mock.return_value
26
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
27
+ instance.describe_regions.return_value = {
28
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
29
+ }
30
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
31
+
32
+ sns.SNSNotification(ctx, imagename).publish()
33
+ assert instance.publish.called == called_sns_publish
34
+ assert instance.publish.call_count == publish_call_count
35
+
36
+
37
+ @pytest.mark.parametrize(
38
+ "imagename",
39
+ [
40
+ ("test-image-10"),
41
+ ("test-image-11"),
42
+ ("test-image-12"),
43
+ ],
44
+ )
45
+ def test_sns_publish_fail_with_invalid_topic(imagename):
46
+ """
47
+ Test the send_notification logic
48
+ """
49
+ with patch("boto3.client") as bclient_mock:
50
+ instance = bclient_mock.return_value
51
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
52
+ instance.describe_regions.return_value = {
53
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
54
+ }
55
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
56
+
57
+ # topic1 is invalid topic
58
+ def side_effect(*args, **kwargs):
59
+ topic_arn = kwargs.get("TopicArn")
60
+ if "topic1" in topic_arn and "us-east-1" in topic_arn:
61
+ error_reponse = {
62
+ "Error": {
63
+ "Code": "NotFoundException",
64
+ "Message": "An error occurred (NotFound) when calling the Publish operation: "
65
+ "Topic does not exist.",
66
+ }
67
+ }
68
+ raise botocore.exceptions.ClientError(error_reponse, "")
69
+
70
+ instance.publish.side_effect = side_effect
71
+
72
+ with pytest.raises(exceptions.AWSNotificationException):
73
+ sns.SNSNotification(ctx, imagename).publish()
74
+
75
+
76
+ @pytest.mark.parametrize(
77
+ "imagename",
78
+ [
79
+ ("test-image-10"),
80
+ ("test-image-11"),
81
+ ("test-image-12"),
82
+ ],
83
+ )
84
+ def test_sns_publish_fail_with_unauthorized_user(imagename):
85
+ """
86
+ Test the send_notification logic
87
+ """
88
+ with patch("boto3.client") as bclient_mock:
89
+ instance = bclient_mock.return_value
90
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
91
+ instance.describe_regions.return_value = {
92
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
93
+ }
94
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
95
+
96
+ error_reponse = {
97
+ "Error": {
98
+ "Code": "AuthorizationError",
99
+ "Message": "User are not authorized perform SNS Notification service",
100
+ }
101
+ }
102
+ instance.publish.side_effect = botocore.exceptions.ClientError(error_reponse, "")
103
+
104
+ with pytest.raises(exceptions.AWSAuthorizationException):
105
+ sns.SNSNotification(ctx, imagename).publish()
106
+
107
+
108
+ @pytest.mark.parametrize(
109
+ "imagename, partition, regions_in_partition, expected",
110
+ [
111
+ (
112
+ "test-image-10",
113
+ "aws-cn",
114
+ ["cn-north1", "cn-northwest-1"],
115
+ [],
116
+ ),
117
+ (
118
+ "test-image-11",
119
+ "aws",
120
+ ["us-east-1", "eu-central-1"],
121
+ [
122
+ "arn:aws:sns:us-east-1:1234:topic1",
123
+ "arn:aws:sns:eu-central-1:1234:topic2",
124
+ ],
125
+ ),
126
+ (
127
+ "test-image-12",
128
+ "aws",
129
+ ["us-east-1", "eu-central-1"],
130
+ [
131
+ "arn:aws:sns:us-east-1:1234:topic1",
132
+ "arn:aws:sns:eu-central-1:1234:topic1",
133
+ ],
134
+ ),
135
+ ],
136
+ )
137
+ def test_sns__get_topic_arn(imagename, partition, regions_in_partition, expected):
138
+ """
139
+ Test the send_notification logic
140
+ """
141
+ with patch("boto3.client") as bclient_mock:
142
+ instance = bclient_mock.return_value
143
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
144
+ sns_conf = ctx.conf["images"][imagename]["sns"]
145
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
146
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
147
+
148
+ instance.get_caller_identity.return_value = {"Account": "1234", "Arn": f"arn:{partition}:iam::1234:user/test"}
149
+
150
+ topic_arns = []
151
+ for topic in sns_conf:
152
+ for topic_name, topic_conf in topic.items():
153
+ sns_regions = sns.SNSNotification(ctx, imagename)._sns_regions(topic_conf)
154
+ for region in sns_regions:
155
+ res_arn = sns.SNSNotification(ctx, imagename)._get_topic_arn(topic_name, region)
156
+ topic_arns.append(res_arn)
157
+
158
+ assert topic_arns == expected
159
+
160
+
161
+ @pytest.mark.parametrize(
162
+ "imagename,regions_in_partition,regions_expected",
163
+ [
164
+ ("test-image-10", ["us-east-1", "eu-west-1"], {"topic1": ["us-east-1"]}),
165
+ (
166
+ "test-image-11",
167
+ ["us-east-1", "eu-west-1"],
168
+ {"topic1": ["us-east-1"], "topic2": []},
169
+ ),
170
+ ("test-image-12", ["eu-northwest-1", "ap-southeast-1"], {"topic1": ["eu-northwest-1", "ap-southeast-1"]}),
171
+ ],
172
+ )
173
+ def test_sns_regions(imagename, regions_in_partition, regions_expected):
174
+ """
175
+ Test the regions for a given image
176
+ """
177
+ with patch("boto3.client") as bclient_mock:
178
+ instance = bclient_mock.return_value
179
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
180
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
181
+ sns_conf = ctx.conf["images"][imagename]["sns"]
182
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
183
+
184
+ sns_regions = {}
185
+ for topic in sns_conf:
186
+ for topic_name, topic_conf in topic.items():
187
+ sns_regions[topic_name] = sns.SNSNotification(ctx, imagename)._sns_regions(topic_conf)
188
+
189
+ assert sns_regions == regions_expected
@@ -1,22 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: awspub
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: Publish images to AWS EC2
5
5
  Home-page: https://github.com/canonical/awspub
6
6
  License: GPL-3.0-or-later
7
7
  Keywords: AWS,EC2,publication
8
8
  Author: Thomas Bechtold
9
9
  Author-email: thomasbechtold@jpberlin.de
10
- Requires-Python: >=3.8.1,<4.0.0
10
+ Requires-Python: >=3.10,<4.0
11
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Dist: autodoc-pydantic (>=2.0.1,<3.0.0)
18
18
  Requires-Dist: boto3
19
- Requires-Dist: boto3-stubs[essential,marketplace-catalog,s3,ssm] (>=1.28.83,<2.0.0)
19
+ Requires-Dist: boto3-stubs[essential,marketplace-catalog,s3,sns,ssm,sts] (>=1.28.83,<2.0.0)
20
20
  Requires-Dist: pydantic (>=2,<3)
21
21
  Requires-Dist: ruamel-yaml (>=0.18.6,<0.19.0)
22
22
  Project-URL: Repository, https://github.com/canonical/awspub
@@ -44,20 +44,3 @@ License
44
44
 
45
45
  The project uses `GPL-3.0` as license.
46
46
 
47
- Doing a new release
48
- ===================
49
-
50
- New releases are mostly automated.
51
-
52
- pypi
53
- ----
54
-
55
- For a new release on pypi, create a new tag (following semantic versioning)
56
- with a `v` as prefix (eg. `v0.2.1`).
57
-
58
- snapstore
59
- ---------
60
-
61
- The latest git commit will be automatically build and published to the `latest/edge`
62
- channel. Manually promote from `latest/edge` to `latest/stable`.
63
-
@@ -1,33 +1,35 @@
1
1
  awspub/__init__.py,sha256=7hgLrq6k53yaJrjFe7X5Cm45z3SIc1Vxocb5k3G8xPc,124
2
2
  awspub/api.py,sha256=d1gx9LdqdYXRLf8yZ_spIz_93WhB2GNnCG_x3ABrMkI,6497
3
3
  awspub/cli/__init__.py,sha256=-zCBEbnt5zbvSZ8PxQALpPAy0CiQUf-qZnikJ7U4Sf0,5621
4
- awspub/common.py,sha256=M_Ibw8DoAHG3oLoK5qRUggEjI7kJSSslC7r9VySe8vk,562
5
- awspub/configmodels.py,sha256=DMrC3N8V_zj2SuBRJu27dxxY4Um5rcemWFFqMlA6j9E,7824
4
+ awspub/common.py,sha256=RFNPfPAw_C0mtdEyBoWWMP6n6CyESdakXhzAvDoyAfQ,2340
5
+ awspub/configmodels.py,sha256=_6I_-1YzfJiSY81QWK-T3afiQUMx810lQQ3EjoEVuNg,9396
6
6
  awspub/context.py,sha256=LDkp9Sz5AqRxQq70ICgFIJn5g2qrc5qiVawTyS_rXZE,4064
7
- awspub/exceptions.py,sha256=SbGf9XyiGlj6estlraAwWAKLtuEfzwEuAbHXYiCiJD0,447
8
- awspub/image.py,sha256=9ms0sEiDmjrZ2rWpgS10-zCD25yw49dh5r7NDv0-Q7o,27589
7
+ awspub/exceptions.py,sha256=2JUEPhZ3sir2NoAuqteFYlh84LsrD7vaeIpkiWtiBpc,556
8
+ awspub/image.py,sha256=JVJEOnlGR34QUxH-APiWtxTmF6rp2EV846Uimi8o-uU,26878
9
9
  awspub/image_marketplace.py,sha256=oiD7yNU5quG5CQG9Ql5Ut9hLWA1yewg6qVwTbyadGwc,5314
10
10
  awspub/s3.py,sha256=ivR8DuAkYilph73EjFkTgUelkXxU7pZfosnsHHyoZkQ,11274
11
11
  awspub/snapshot.py,sha256=V5e_07SnmCwEPjRmwZh43spWparhH8X4ugG16uQfGuo,10040
12
+ awspub/sns.py,sha256=q_XfH_-fMC3giWmGLA9ANspZoI2spiqT9tgpAHjzQi0,3970
12
13
  awspub/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  awspub/tests/fixtures/config-invalid-s3-extra.yaml,sha256=TdgqE-quxgueXS9L8ixsRuG6eTVfqalZ41G3JNCWn58,288
14
15
  awspub/tests/fixtures/config-minimal.yaml,sha256=oHupXHYQXxmqgN2qFCAwvxzR7Bch-3yScrmMXeMIICE,176
15
16
  awspub/tests/fixtures/config-valid-nonawspub.yaml,sha256=Md-YINQQRo3kveikUxk8Co9BYIZfDftmPT2LmIqoTL4,330
16
17
  awspub/tests/fixtures/config1.vmdk,sha256=YlJHVAi5-e5kRSthHXBqB4gxqZsSPbadFE2HigSIoKg,65536
17
- awspub/tests/fixtures/config1.yaml,sha256=QUxX7j7SNP40CeSIRFcXm54Ef2CmzgDeWfYYy_THlM0,3256
18
+ awspub/tests/fixtures/config1.yaml,sha256=knkwwBiImVa8vIxurFbKP6hAB4-84_L2bXcxwdsraig,4609
18
19
  awspub/tests/fixtures/config2-mapping.yaml,sha256=lqJE0ej9DdGsE8O5dqG5PX7bOJrY4nMciXoOzMzV-so,31
19
20
  awspub/tests/fixtures/config2.yaml,sha256=m2v-n1T-XPGDHyrJXArC_rYV-ZPMr9cgzHkLXiSRuDs,1250
20
21
  awspub/tests/fixtures/config3-duplicate-keys.yaml,sha256=Cn0tTQawpEFocDNpWxDz1651uQa7aw88XjNyPcCG4iQ,324
21
- awspub/tests/test_api.py,sha256=7MKm2aCtcvHJ0x_o2qinljfL9xFBWnasUnVpBxB37w8,2504
22
+ awspub/tests/test_api.py,sha256=eC8iqpGFgFygSbqlyWLiLjSW7TwZeEtngJ-g7CPQT7I,2603
22
23
  awspub/tests/test_cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- awspub/tests/test_common.py,sha256=kytMUU47uZYYe302XswdO15qX_i1vO2LS5n96--TcSU,478
24
+ awspub/tests/test_common.py,sha256=K61eBmBt8NPqyME8co_dt6dr_yvlw3UZ54klcw7m2RA,1217
24
25
  awspub/tests/test_context.py,sha256=wMXQqj4vi2U3q5w1xPV-stB3mp3K6puUyXhsShJG4wA,3115
25
- awspub/tests/test_image.py,sha256=tMcMx8rnx0q3oeRBA3JSeOHVxnnUGG_AAHVsZ7DWNYw,19083
26
+ awspub/tests/test_image.py,sha256=B1X4iy4B9o45InnsEbU8YLghhq7BZKvkU1hVC7l0j1k,19523
26
27
  awspub/tests/test_image_marketplace.py,sha256=JP7PrFjix1AyQg7eEaQ-wCROVoIOb873koseniOqGQQ,1456
27
28
  awspub/tests/test_s3.py,sha256=UJL8CQDEvhA42MwPGeSvSbQFj8h86c1LrLFDvcMcRws,2857
28
29
  awspub/tests/test_snapshot.py,sha256=8KPTqGVyzrpivWuq3HE7ZhgtLllcr3rA_3hZcxu2xjg,4123
29
- awspub-0.0.7.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
30
- awspub-0.0.7.dist-info/METADATA,sha256=AHGXMR-iFQqINhVpdGyvFTkFChcYfzqdHf5s-tEjmi8,1773
31
- awspub-0.0.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- awspub-0.0.7.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
33
- awspub-0.0.7.dist-info/RECORD,,
30
+ awspub/tests/test_sns.py,sha256=XdZh0ETwRHSp_77UFkot-07BlS8pKVMEJIs2HS9EdaQ,6622
31
+ awspub-0.0.9.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
32
+ awspub-0.0.9.dist-info/METADATA,sha256=3QPgFi4EUPU2fR5M8t3xU69Sb70QDvmTpwRYzmwCQt0,1405
33
+ awspub-0.0.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
34
+ awspub-0.0.9.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
35
+ awspub-0.0.9.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any