Skip to content

Commit 6d7d934

Browse files
Merge branch 'master' of https://github.com/datajoint/datajoint-python into issue151
2 parents bc12b11 + 61dab59 commit 6d7d934

File tree

11 files changed

+176
-67
lines changed

11 files changed

+176
-67
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
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+
<<<<<<< HEAD
67
* Bugfix - Deletes and drops can cascade to part from master only. (#151 and #374) PR #957
8+
=======
9+
* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
10+
>>>>>>> 61dab59fe59a408b018bf2a6330ec0ce07c2ad42
711
812
### 0.13.2 -- May 7, 2021
913
* Update `setuptools_certificate` dependency to new name `otumat`

LNX-docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ services:
7777
- -c
7878
- |
7979
set -e
80+
wget -P /home/dja/.local/bin/ https://dl.min.io/client/mc/release/linux-amd64/mc
81+
chmod +x /home/dja/.local/bin/mc
82+
mc alias set myminio/ http://minio:9000 datajoint datajoint
8083
pip install --user -r test_requirements.txt
8184
pip install -e .
8285
pip freeze | grep datajoint

README.md

Lines changed: 1 addition & 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
```

datajoint/external.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,12 @@ def used(self):
314314
return self & [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
315315
for ref in self.references]
316316

317-
def delete(self, *, delete_external_files=None, limit=None, display_progress=True):
317+
def delete(self, *, delete_external_files=None, limit=None, display_progress=True, errors_as_string=True):
318318
"""
319319
:param delete_external_files: True or False. If False, only the tracking info is removed from the
320320
external store table but the external files remain intact. If True, then the external files
321321
themselves are deleted too.
322+
:param errors_as_string: If True any errors returned when deleting from external files will be strings
322323
:param limit: (integer) limit the number of items to delete
323324
:param display_progress: if True, display progress as files are cleaned up
324325
:return: if deleting external files, returns errors
@@ -346,7 +347,8 @@ def delete(self, *, delete_external_files=None, limit=None, display_progress=Tru
346347
try:
347348
self._remove_external_file(external_path)
348349
except Exception as error:
349-
error_list.append((uuid, external_path, str(error)))
350+
error_list.append((uuid, external_path,
351+
str(error) if errors_as_string else error))
350352
return error_list
351353

352354

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)

datajoint/table.py

Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -361,59 +361,7 @@ def delete_quick(self, get_count=False):
361361
self._log(query[:255])
362362
return count
363363

364-
def _delete_cascade(self):
365-
"""service function to perform cascading deletes recursively."""
366-
max_attempts = 50
367-
for _ in range(max_attempts):
368-
try:
369-
delete_count = self.delete_quick(get_count=True)
370-
except IntegrityError as error:
371-
match = foreign_key_error_regexp.match(error.args[0]).groupdict()
372-
if "`.`" not in match['child']: # if schema name missing, use self
373-
match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0],
374-
match['child'])
375-
if match['pk_attrs'] is not None: # fully matched, adjusting the keys
376-
match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
377-
match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
378-
else: # only partially matched, querying with constraint to determine keys
379-
match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(
380-
list, zip(*self.connection.query(constraint_info_query, args=(
381-
match['name'].strip('`'),
382-
*[_.strip('`') for _ in match['child'].split('`.`')]
383-
)).fetchall())))
384-
match['parent'] = match['parent'][0]
385-
386-
# Avoid deleting from child before master (See issue #151)
387-
master = get_master(match['child'])
388-
if master and self.full_table_name != master:
389-
raise DataJointError(
390-
'Attempt to delete from part table {part} before deleting from '
391-
'its master. Delete from {master} first.'.format(
392-
part=match['child'], master=master))
393-
394-
# Restrict child by self if
395-
# 1. if self's restriction attributes are not in child's primary key
396-
# 2. if child renames any attributes
397-
# Otherwise restrict child by self's restriction.
398-
child = FreeTable(self.connection, match['child'])
399-
if set(self.restriction_attributes) <= set(child.primary_key) and \
400-
match['fk_attrs'] == match['pk_attrs']:
401-
child._restriction = self._restriction
402-
elif match['fk_attrs'] != match['pk_attrs']:
403-
child &= self.proj(**dict(zip(match['fk_attrs'],
404-
match['pk_attrs'])))
405-
else:
406-
child &= self.proj()
407-
child._delete_cascade()
408-
else:
409-
print("Deleting {count} rows from {table}".format(
410-
count=delete_count, table=self.full_table_name))
411-
break
412-
else:
413-
raise DataJointError('Exceeded maximum number of delete attempts.')
414-
return delete_count
415-
416-
def delete(self, transaction=True, safemode=None):
364+
def delete(self, transaction=True, safemode=None, force_parts=False):
417365
"""
418366
Deletes the contents of the table and its dependent tables, recursively.
419367
@@ -422,8 +370,56 @@ def delete(self, transaction=True, safemode=None):
422370
within another transaction.
423371
:param safemode: If True, prohibit nested transactions and prompt to confirm. Default
424372
is dj.config['safemode'].
373+
:param force_parts: Delete from parts even when not deleting from their masters.
425374
:return: number of deleted rows (excluding those from dependent tables)
426375
"""
376+
deleted = set()
377+
378+
def cascade(table):
379+
"""service function to perform cascading deletes recursively."""
380+
max_attempts = 50
381+
for _ in range(max_attempts):
382+
try:
383+
delete_count = table.delete_quick(get_count=True)
384+
except IntegrityError as error:
385+
match = foreign_key_error_regexp.match(error.args[0]).groupdict()
386+
if "`.`" not in match['child']: # if schema name missing, use table
387+
match['child'] = '{}.{}'.format(table.full_table_name.split(".")[0],
388+
match['child'])
389+
if match['pk_attrs'] is not None: # fully matched, adjusting the keys
390+
match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
391+
match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
392+
else: # only partially matched, querying with constraint to determine keys
393+
match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(
394+
list, zip(*table.connection.query(constraint_info_query, args=(
395+
match['name'].strip('`'),
396+
*[_.strip('`') for _ in match['child'].split('`.`')]
397+
)).fetchall())))
398+
match['parent'] = match['parent'][0]
399+
400+
# Restrict child by table if
401+
# 1. if table's restriction attributes are not in child's primary key
402+
# 2. if child renames any attributes
403+
# Otherwise restrict child by table's restriction.
404+
child = FreeTable(table.connection, match['child'])
405+
if set(table.restriction_attributes) <= set(child.primary_key) and \
406+
match['fk_attrs'] == match['pk_attrs']:
407+
child._restriction = table._restriction
408+
elif match['fk_attrs'] != match['pk_attrs']:
409+
child &= table.proj(**dict(zip(match['fk_attrs'],
410+
match['pk_attrs'])))
411+
else:
412+
child &= table.proj()
413+
cascade(child)
414+
else:
415+
deleted.add(table.full_table_name)
416+
print("Deleting {count} rows from {table}".format(
417+
count=delete_count, table=table.full_table_name))
418+
break
419+
else:
420+
raise DataJointError('Exceeded maximum number of delete attempts.')
421+
return delete_count
422+
427423
safemode = config['safemode'] if safemode is None else safemode
428424

429425
# Start transaction
@@ -440,12 +436,23 @@ def delete(self, transaction=True, safemode=None):
440436

441437
# Cascading delete
442438
try:
443-
delete_count = self._delete_cascade()
439+
delete_count = cascade(self)
444440
except:
445441
if transaction:
446442
self.connection.cancel_transaction()
447443
raise
448444

445+
if not force_parts:
446+
# Avoid deleting from child before master (See issue #151)
447+
for part in deleted:
448+
master = get_master(part)
449+
if master and master not in deleted:
450+
if transaction:
451+
self.connection.cancel_transaction()
452+
raise DataJointError(
453+
'Attempt to delete part table {part} before deleting from '
454+
'its master {master} first.'.format(part=part, master=master))
455+
449456
# Confirm and commit
450457
if delete_count == 0:
451458
if safemode:

datajoint/user_tables.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,10 @@ def delete(self, force=False):
173173
unless force is True, prohibits direct deletes from parts.
174174
"""
175175
if force:
176-
super().delete()
176+
super().delete(force_parts=True)
177177
else:
178-
raise DataJointError('Cannot delete from a Part directly. Delete from master instead')
178+
raise DataJointError(
179+
'Cannot delete from a Part directly. Delete from master instead')
179180

180181
def drop(self, force=False):
181182
"""

docs-parts/intro/Releases_lang1.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
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+
<<<<<<< HEAD
56
* Bugfix - Deletes and drops can cascade to part from master only. (#151 and #374) PR #957
7+
=======
8+
* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
9+
>>>>>>> 61dab59fe59a408b018bf2a6330ec0ce07c2ad42
610

711
0.13.2 -- May 7, 2021
812
----------------------

local-docker-compose.yml

Lines changed: 4 additions & 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
@@ -82,6 +82,9 @@ services:
8282
- -c
8383
- |
8484
set -e
85+
wget -P /home/dja/.local/bin/ https://dl.min.io/client/mc/release/linux-amd64/mc
86+
chmod +x /home/dja/.local/bin/mc
87+
mc alias set myminio/ http://minio:9000 datajoint datajoint
8588
pip install --user nose nose-cov coveralls flake8 ptvsd
8689
pip install -e .
8790
pip freeze | grep datajoint

0 commit comments

Comments
 (0)