Skip to content

Commit cc144a6

Browse files
Merge branch 'master' into mp
2 parents f6a5072 + 7764467 commit cc144a6

File tree

10 files changed

+94
-25
lines changed

10 files changed

+94
-25
lines changed

.github/workflows/development.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
PY_VER: ${{matrix.py_ver}}
4141
MYSQL_VER: ${{matrix.mysql_ver}}
4242
ALPINE_VER: "3.10"
43-
MINIO_VER: RELEASE.2019-09-26T19-42-35Z
43+
MINIO_VER: RELEASE.2021-09-03T03-56-13Z
4444
COMPOSE_HTTP_TIMEOUT: "120"
4545
COVERALLS_SERVICE_NAME: travis-ci
4646
COVERALLS_REPO_TOKEN: fd0BoXG46TPReEem0uMy7BJO5j0w1MQiY

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### 0.13.3 -- TBD
44
* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
55
* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
6+
* Bugfix - `ExternalTable.delete` should not remove row on error (#953) PR #956
7+
* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
68

79
### 0.13.2 -- May 7, 2021
810
* Update `setuptools_certificate` dependency to new name `otumat`

LNX-docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ services:
3232
interval: 1s
3333
fakeservices.datajoint.io:
3434
<<: *net
35-
image: datajoint/nginx:v0.0.17
35+
image: datajoint/nginx:v0.0.18
3636
environment:
3737
- ADD_db_TYPE=DATABASE
3838
- ADD_db_ENDPOINT=db:3306

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ A number of labs are currently adopting DataJoint and we are quickly getting the
108108
PY_VER=3.7
109109
ALPINE_VER=3.10
110110
MYSQL_VER=5.7
111-
MINIO_VER=RELEASE.2019-09-26T19-42-35Z
111+
MINIO_VER=RELEASE.2021-09-03T03-56-13Z
112112
UID=1000
113113
GID=1000
114114
```
@@ -136,6 +136,8 @@ GID=1000
136136
* Add entry in `/etc/hosts` for `127.0.0.1 fakeservices.datajoint.io`
137137

138138

139+
140+
139141
### Launch Jupyter Notebook for Interactive Use
140142
* Navigate to `localhost:8888`
141143
* Input Jupyter password

datajoint/external.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ def _remove_external_file(self, external_path):
127127
if self.spec['protocol'] == 's3':
128128
self.s3.remove_object(external_path)
129129
elif self.spec['protocol'] == 'file':
130-
Path(external_path).unlink()
130+
try:
131+
Path(external_path).unlink()
132+
except FileNotFoundError:
133+
pass
131134

132135
def exists(self, external_filepath):
133136
"""
@@ -314,11 +317,12 @@ def used(self):
314317
return self & [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
315318
for ref in self.references]
316319

317-
def delete(self, *, delete_external_files=None, limit=None, display_progress=True):
320+
def delete(self, *, delete_external_files=None, limit=None, display_progress=True, errors_as_string=True):
318321
"""
319322
:param delete_external_files: True or False. If False, only the tracking info is removed from the
320323
external store table but the external files remain intact. If True, then the external files
321324
themselves are deleted too.
325+
:param errors_as_string: If True any errors returned when deleting from external files will be strings
322326
:param limit: (integer) limit the number of items to delete
323327
:param display_progress: if True, display progress as files are cleaned up
324328
:return: if deleting external files, returns errors
@@ -337,16 +341,20 @@ def delete(self, *, delete_external_files=None, limit=None, display_progress=Tru
337341
# delete items one by one, close to transaction-safe
338342
error_list = []
339343
for uuid, external_path in items:
340-
try:
341-
count = (self & {'hash': uuid}).delete_quick(get_count=True) # optimize
342-
except Exception:
343-
pass # if delete failed, do not remove the external file
344-
else:
345-
assert count in (0, 1)
344+
row = (self & {'hash': uuid}).fetch()
345+
if row.size:
346346
try:
347-
self._remove_external_file(external_path)
348-
except Exception as error:
349-
error_list.append((uuid, external_path, str(error)))
347+
(self & {'hash': uuid}).delete_quick()
348+
except Exception:
349+
pass # if delete failed, do not remove the external file
350+
else:
351+
try:
352+
self._remove_external_file(external_path)
353+
except Exception as error:
354+
# adding row back into table after failed delete
355+
self.insert1(row[0], skip_duplicates=True)
356+
error_list.append((uuid, external_path,
357+
str(error) if errors_as_string else error))
350358
return error_list
351359

352360

datajoint/s3.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,11 @@ def get_size(self, name):
7676
except minio.error.S3Error as e:
7777
if e.code == 'NoSuchKey':
7878
raise errors.MissingExternalFile
79-
else:
80-
raise e
79+
raise e
8180

8281
def remove_object(self, name):
8382
logger.debug('remove_object: {}:{}'.format(self.bucket, name))
8483
try:
8584
self.client.remove_object(self.bucket, str(name))
86-
except minio.ResponseError:
87-
return errors.DataJointError('Failed to delete %s from s3 storage' % name)
85+
except minio.error.MinioException:
86+
raise errors.DataJointError('Failed to delete %s from s3 storage' % name)

docs-parts/intro/Releases_lang1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
----------------------
33
* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
44
* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
5+
* Bugfix - `ExternalTable.delete` should not remove row on error (#953) PR #956
6+
* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
57

68
0.13.2 -- May 7, 2021
79
----------------------

local-docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ services:
3434
interval: 1s
3535
fakeservices.datajoint.io:
3636
<<: *net
37-
image: datajoint/nginx:v0.0.17
37+
image: datajoint/nginx:v0.0.18
3838
environment:
3939
- ADD_db_TYPE=DATABASE
4040
- ADD_db_ENDPOINT=db:3306

tests/test_external.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
from nose.tools import assert_true, assert_equal
44
from datajoint.external import ExternalTable
55
from datajoint.blob import pack, unpack
6-
7-
from .schema_external import schema
86
import datajoint as dj
9-
from .schema_external import stores_config
10-
from .schema_external import SimpleRemote
7+
from .schema_external import stores_config, SimpleRemote, Simple, schema
8+
import os
9+
1110
current_location_s3 = dj.config['stores']['share']['location']
1211
current_location_local = dj.config['stores']['local']['location']
1312

@@ -47,6 +46,9 @@ def test_s3_leading_slash(index=100, store='share'):
4746
"""
4847
s3 external storage configured with leading slash
4948
"""
49+
50+
oldConfig = dj.config['stores'][store]['location']
51+
5052
value = np.array([1, 2, 3])
5153

5254
id = index
@@ -97,9 +99,28 @@ def test_s3_leading_slash(index=100, store='share'):
9799
assert_true(np.array_equal(
98100
value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
99101

102+
dj.config['stores'][store]['location'] = oldConfig
103+
100104

101105
def test_file_leading_slash():
102106
"""
103107
file external storage configured with leading slash
104108
"""
105109
test_s3_leading_slash(index=200, store='local')
110+
111+
112+
def test_remove_fail():
113+
# https://github.com/datajoint/datajoint-python/issues/953
114+
data = dict(simple=2, item=[1, 2, 3])
115+
Simple.insert1(data)
116+
path1 = dj.config['stores']['local']['location'] + '/djtest_extern/4/c/'
117+
currentMode = int(oct(os.stat(path1).st_mode), 8)
118+
os.chmod(path1, 0o40555)
119+
(Simple & 'simple=2').delete()
120+
listOfErrors = schema.external['local'].delete(delete_external_files=True)
121+
assert len(listOfErrors) == 1, 'unexpected number of errors'
122+
assert len(schema.external['local'] & dict(hash=listOfErrors[0][0])) == 1, \
123+
'unexpected number of rows in external table'
124+
# ---------------------CLEAN UP--------------------
125+
os.chmod(path1, currentMode)
126+
listOfErrors = schema.external['local'].delete(delete_external_files=True)

tests/test_s3.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from . import S3_CONN_INFO
2-
32
from minio import Minio
43
import urllib3
54
import certifi
6-
from nose.tools import assert_true
5+
from nose.tools import assert_true, raises
6+
from .schema_external import schema, SimpleRemote
7+
from datajoint.errors import DataJointError
8+
from datajoint.hash import uuid_from_buffer
9+
from datajoint.blob import pack
710

811

912
class TestS3:
@@ -51,3 +54,35 @@ def test_connection_secure():
5154
http_client=http_client)
5255

5356
assert_true(minio_client.bucket_exists(S3_CONN_INFO['bucket']))
57+
58+
@staticmethod
59+
@raises(DataJointError)
60+
def test_remove_object_exception():
61+
# https://github.com/datajoint/datajoint-python/issues/952
62+
63+
# Insert some test data and remove it so that the external table is populated
64+
test = [1, [1, 2, 3]]
65+
SimpleRemote.insert1(test)
66+
SimpleRemote.delete()
67+
68+
# Save the old external table minio client
69+
old_client = schema.external['share'].s3.client
70+
71+
# Apply our new minio client which has a user that does not exist
72+
schema.external['share'].s3.client = Minio(
73+
'minio:9000',
74+
access_key='jeffjeff',
75+
secret_key='jeffjeff',
76+
secure=False)
77+
78+
# This method returns a list of errors
79+
error_list = schema.external['share'].delete(delete_external_files=True,
80+
errors_as_string=False)
81+
82+
# Teardown
83+
schema.external['share'].s3.client = old_client
84+
schema.external['share'].delete(delete_external_files=True)
85+
86+
# Raise the error we want if the error matches the expected uuid
87+
if str(error_list[0][0]) == str(uuid_from_buffer(pack(test[1]))):
88+
raise error_list[0][2]

0 commit comments

Comments
 (0)