Merge ~aciba/simplestreams:migrate-to-python3-boto3 into simplestreams:master

Proposed by Alberto Contreras
Status: Merged
Approved by: Paride Legovini
Approved revision: a3e76ce0786fc2087f60a0f2541f75c472cc6e0f
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~aciba/simplestreams:migrate-to-python3-boto3
Merge into: simplestreams:master
Diff against target: 129 lines (+34/-33)
2 files modified
debian/control (+1/-1)
simplestreams/objectstores/s3.py (+33/-32)
Reviewer Review Type Date Requested Status
Paride Legovini Approve
Server Team CI bot continuous-integration Approve
simplestreams-dev Pending
Review via email: mp+464027@code.launchpad.net

Commit message

feat(s3objectstore)!: migrate to python3-boto3

Migrate S3ObjectStore from python3-boto to python3-boto3.

BREAKING CHANGE: Some of the methods did not capture all the underlying
boto expeceptions, and exceptions are different between boto and boto3.
Thus, any callsite handling boto exceptions may have to adjust them.

BREAKING CHANGE: S3ObjectStore.bucket did expose a boto class which is
different in boto3. `bucket` has been removed to not couple
simplestreams' API with boto{,3}'s API. S3ObjectStore.bucketname is
still exposed, so callsites can refer to it however they want.

LP: #2052437

To post a comment you must log in.
Revision history for this message
Alberto Contreras (aciba) wrote :

I have tested this with the following python script. Make sure to have your bucket pre-created and your aws config set.

```py
import errno
import hashlib
from functools import partial

from simplestreams.objectstores.s3 import S3ObjectStore

sos = S3ObjectStore("s3://test-bucket-1712739788/myprefix/")

content = "content"
path = "path"
sos.insert_content(path, content)
sos.insert_content(path, content)

assert sos.source(path).read() == b"content"

md5 = hashlib.md5(content.encode("utf-8")).hexdigest()

assert sos.exists_with_checksum(path, {}) is False
assert sos.exists_with_checksum(path, {"md5": "asdf"}) is False
assert sos.exists_with_checksum(path, {"md5": md5})

sos.remove("path")
sos.remove("path")
sos.remove("path")

assert sos.exists_with_checksum(path, {"md5": md5}) is False

try:
    sos.source(path).read()
except IOError as ex:
    assert ex.errno == errno.ENOENT
    assert str(ex) == f"Unable to open {path}"

with open(path, "wb") as f:
    f.write(content.encode("utf-8"))
sos.insert(path, partial(open, mode="rb"))
assert sos.source(path).read() == b"content"
sos.remove("path")
```

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Paride Legovini (paride) wrote :

LGTM, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/control b/debian/control
2index 51372b8..a595408 100644
3--- a/debian/control
4+++ b/debian/control
5@@ -31,7 +31,7 @@ Description: Library and tools for using Simple Streams data
6
7 Package: python3-simplestreams
8 Architecture: all
9-Depends: gnupg, python3-boto, ${misc:Depends}, ${python3:Depends}
10+Depends: gnupg, python3-boto3, ${misc:Depends}, ${python3:Depends}
11 Suggests: python3-requests (>= 1.1)
12 Description: Library and tools for using Simple Streams data
13 This package provides a client for interacting with simple
14diff --git a/simplestreams/objectstores/s3.py b/simplestreams/objectstores/s3.py
15index f1e9602..da771f9 100644
16--- a/simplestreams/objectstores/s3.py
17+++ b/simplestreams/objectstores/s3.py
18@@ -15,22 +15,17 @@
19 # You should have received a copy of the GNU Affero General Public License
20 # along with Simplestreams. If not, see <http://www.gnu.org/licenses/>.
21
22-import boto.exception
23-import boto.s3
24-import boto.s3.connection
25-from contextlib import closing
26 import errno
27 import tempfile
28
29+import boto3
30+
31 import simplestreams.objectstores as objectstores
32 import simplestreams.contentsource as cs
33
34
35 class S3ObjectStore(objectstores.ObjectStore):
36
37- _bucket = None
38- _connection = None
39-
40 def __init__(self, prefix):
41 # expect 's3://bucket/path_prefix'
42 self.prefix = prefix
43@@ -40,19 +35,7 @@ class S3ObjectStore(objectstores.ObjectStore):
44 path = prefix
45
46 (self.bucketname, self.path_prefix) = path.split("/", 1)
47-
48- @property
49- def _conn(self):
50- if not self._connection:
51- self._connection = boto.s3.connection.S3Connection()
52-
53- return self._connection
54-
55- @property
56- def bucket(self):
57- if not self._bucket:
58- self._bucket = self._conn.get_bucket(self.bucketname)
59- return self._bucket
60+ self._client = boto3.client("s3")
61
62 def insert(self, path, reader, checksums=None, mutable=True, size=None):
63 # store content from reader.read() into path, expecting result checksum
64@@ -64,36 +47,54 @@ class S3ObjectStore(objectstores.ObjectStore):
65 tfile.write(buf)
66 if len(buf) != self.read_size:
67 break
68- with closing(self.bucket.new_key(self.path_prefix + path)) as key:
69- key.set_contents_from_file(tfile)
70+ tfile.seek(0)
71+ self.insert_content(
72+ path, tfile, checksums=checksums, mutable=mutable
73+ )
74 finally:
75 tfile.close()
76
77 def insert_content(self, path, content, checksums=None, mutable=True):
78- with closing(self.bucket.new_key(self.path_prefix + path)) as key:
79- key.set_contents_from_string(content)
80+ self._client.put_object(
81+ Body=content, Bucket=self.bucketname, Key=self.path_prefix + path
82+ )
83
84 def remove(self, path):
85 # remove path from store
86- self.bucket.delete_key(self.path_prefix + path)
87+ self._client.delete_object(
88+ Bucket=self.bucketname, Key=self.path_prefix + path
89+ )
90
91 def source(self, path):
92 # essentially return an 'open(path, r)'
93- key = self.bucket.get_key(self.path_prefix + path)
94- if not key:
95+ try:
96+ obj_resp = self._client.get_object(
97+ Bucket=self.bucketname, Key=self.path_prefix + path
98+ )
99+ except self._client.exceptions.NoSuchKey:
100+ fd = None
101+ else:
102+ fd = obj_resp.get("Body")
103+ if not fd:
104 myerr = IOError("Unable to open %s" % path)
105 myerr.errno = errno.ENOENT
106 raise myerr
107
108- return cs.FdContentSource(fd=key, url=self.path_prefix + path)
109+ return cs.FdContentSource(fd=fd, url=self.path_prefix + path)
110
111 def exists_with_checksum(self, path, checksums=None):
112- key = self.bucket.get_key(self.path_prefix + path)
113- if key is None:
114+ try:
115+ obj_resp = self._client.get_object(
116+ Bucket=self.bucketname, Key=self.path_prefix + path
117+ )
118+ except self._client.exceptions.NoSuchKey:
119 return False
120
121- if 'md5' in checksums:
122- return checksums['md5'] == key.etag.replace('"', "")
123+ if "md5" in checksums:
124+ md5 = obj_resp["ResponseMetadata"]["HTTPHeaders"]["etag"].replace(
125+ '"', ""
126+ )
127+ return checksums["md5"] == md5
128
129 return False
130

Subscribers

People subscribed via source and target branches