datajoint 0.14.1__py3-none-any.whl → 0.14.3__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.
Potentially problematic release.
This version of datajoint might be problematic. Click here for more details.
- datajoint/__init__.py +5 -2
- datajoint/admin.py +10 -2
- datajoint/autopopulate.py +113 -84
- datajoint/blob.py +6 -4
- datajoint/cli.py +77 -0
- datajoint/condition.py +31 -0
- datajoint/connection.py +4 -1
- datajoint/declare.py +22 -6
- datajoint/dependencies.py +64 -32
- datajoint/diagram.py +62 -48
- datajoint/expression.py +98 -41
- datajoint/external.py +7 -2
- datajoint/fetch.py +31 -51
- datajoint/heading.py +14 -8
- datajoint/preview.py +8 -6
- datajoint/s3.py +2 -1
- datajoint/schemas.py +8 -10
- datajoint/settings.py +9 -0
- datajoint/table.py +54 -12
- datajoint/user_tables.py +27 -0
- datajoint/utils.py +14 -1
- datajoint/version.py +1 -1
- datajoint-0.14.3.dist-info/METADATA +592 -0
- datajoint-0.14.3.dist-info/RECORD +34 -0
- {datajoint-0.14.1.dist-info → datajoint-0.14.3.dist-info}/WHEEL +1 -1
- datajoint-0.14.3.dist-info/entry_points.txt +3 -0
- datajoint-0.14.1.dist-info/METADATA +0 -26
- datajoint-0.14.1.dist-info/RECORD +0 -33
- datajoint-0.14.1.dist-info/datajoint.pub +0 -6
- {datajoint-0.14.1.dist-info → datajoint-0.14.3.dist-info}/LICENSE.txt +0 -0
- {datajoint-0.14.1.dist-info → datajoint-0.14.3.dist-info}/top_level.txt +0 -0
datajoint/fetch.py
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
from functools import partial
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
import logging
|
|
4
3
|
import pandas
|
|
5
4
|
import itertools
|
|
6
|
-
import re
|
|
7
5
|
import json
|
|
8
6
|
import numpy as np
|
|
9
7
|
import uuid
|
|
10
8
|
import numbers
|
|
9
|
+
|
|
10
|
+
from datajoint.condition import Top
|
|
11
11
|
from . import blob, hash
|
|
12
12
|
from .errors import DataJointError
|
|
13
13
|
from .settings import config
|
|
14
14
|
from .utils import safe_write
|
|
15
15
|
|
|
16
|
-
logger = logging.getLogger(__name__.split(".")[0])
|
|
17
|
-
|
|
18
16
|
|
|
19
17
|
class key:
|
|
20
18
|
"""
|
|
@@ -119,21 +117,6 @@ def _get(connection, attr, data, squeeze, download_path):
|
|
|
119
117
|
)
|
|
120
118
|
|
|
121
119
|
|
|
122
|
-
def _flatten_attribute_list(primary_key, attrs):
|
|
123
|
-
"""
|
|
124
|
-
:param primary_key: list of attributes in primary key
|
|
125
|
-
:param attrs: list of attribute names, which may include "KEY", "KEY DESC" or "KEY ASC"
|
|
126
|
-
:return: generator of attributes where "KEY" is replaces with its component attributes
|
|
127
|
-
"""
|
|
128
|
-
for a in attrs:
|
|
129
|
-
if re.match(r"^\s*KEY(\s+[aA][Ss][Cc])?\s*$", a):
|
|
130
|
-
yield from primary_key
|
|
131
|
-
elif re.match(r"^\s*KEY\s+[Dd][Ee][Ss][Cc]\s*$", a):
|
|
132
|
-
yield from (q + " DESC" for q in primary_key)
|
|
133
|
-
else:
|
|
134
|
-
yield a
|
|
135
|
-
|
|
136
|
-
|
|
137
120
|
class Fetch:
|
|
138
121
|
"""
|
|
139
122
|
A fetch object that handles retrieving elements from the table expression.
|
|
@@ -153,7 +136,7 @@ class Fetch:
|
|
|
153
136
|
format=None,
|
|
154
137
|
as_dict=None,
|
|
155
138
|
squeeze=False,
|
|
156
|
-
download_path="."
|
|
139
|
+
download_path=".",
|
|
157
140
|
):
|
|
158
141
|
"""
|
|
159
142
|
Fetches the expression results from the database into an np.array or list of dictionaries and
|
|
@@ -174,13 +157,13 @@ class Fetch:
|
|
|
174
157
|
:param download_path: for fetches that download data, e.g. attachments
|
|
175
158
|
:return: the contents of the table in the form of a structured numpy.array or a dict list
|
|
176
159
|
"""
|
|
177
|
-
if order_by
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
160
|
+
if offset or order_by or limit:
|
|
161
|
+
self._expression = self._expression.restrict(
|
|
162
|
+
Top(
|
|
163
|
+
limit,
|
|
164
|
+
order_by,
|
|
165
|
+
offset,
|
|
166
|
+
)
|
|
184
167
|
)
|
|
185
168
|
|
|
186
169
|
attrs_as_dict = as_dict and attrs
|
|
@@ -212,13 +195,6 @@ class Fetch:
|
|
|
212
195
|
'use "array" or "frame"'.format(format)
|
|
213
196
|
)
|
|
214
197
|
|
|
215
|
-
if limit is None and offset is not None:
|
|
216
|
-
logger.warning(
|
|
217
|
-
"Offset set, but no limit. Setting limit to a large number. "
|
|
218
|
-
"Consider setting a limit explicitly."
|
|
219
|
-
)
|
|
220
|
-
limit = 8000000000 # just a very large number to effect no limit
|
|
221
|
-
|
|
222
198
|
get = partial(
|
|
223
199
|
_get,
|
|
224
200
|
self._expression.connection,
|
|
@@ -244,20 +220,20 @@ class Fetch:
|
|
|
244
220
|
]
|
|
245
221
|
else:
|
|
246
222
|
return_values = [
|
|
247
|
-
|
|
248
|
-
(
|
|
249
|
-
|
|
223
|
+
(
|
|
224
|
+
list(
|
|
225
|
+
(to_dicts if as_dict else lambda x: x)(
|
|
226
|
+
ret[self._expression.primary_key]
|
|
227
|
+
)
|
|
250
228
|
)
|
|
229
|
+
if is_key(attribute)
|
|
230
|
+
else ret[attribute]
|
|
251
231
|
)
|
|
252
|
-
if is_key(attribute)
|
|
253
|
-
else ret[attribute]
|
|
254
232
|
for attribute in attrs
|
|
255
233
|
]
|
|
256
234
|
ret = return_values[0] if len(attrs) == 1 else return_values
|
|
257
235
|
else: # fetch all attributes as a numpy.record_array or pandas.DataFrame
|
|
258
|
-
cur = self._expression.cursor(
|
|
259
|
-
as_dict=as_dict, limit=limit, offset=offset, order_by=order_by
|
|
260
|
-
)
|
|
236
|
+
cur = self._expression.cursor(as_dict=as_dict)
|
|
261
237
|
heading = self._expression.heading
|
|
262
238
|
if as_dict:
|
|
263
239
|
ret = [
|
|
@@ -272,12 +248,14 @@ class Fetch:
|
|
|
272
248
|
else np.dtype(
|
|
273
249
|
[
|
|
274
250
|
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
251
|
+
(
|
|
252
|
+
name,
|
|
253
|
+
type(value),
|
|
254
|
+
) # use the first element to determine blob type
|
|
255
|
+
if heading[name].is_blob
|
|
256
|
+
and isinstance(value, numbers.Number)
|
|
257
|
+
else (name, heading.as_dtype[name])
|
|
258
|
+
)
|
|
281
259
|
for value, name in zip(ret[0], heading.as_dtype.names)
|
|
282
260
|
]
|
|
283
261
|
)
|
|
@@ -353,9 +331,11 @@ class Fetch1:
|
|
|
353
331
|
"fetch1 should only return one tuple. %d tuples found" % len(result)
|
|
354
332
|
)
|
|
355
333
|
return_values = tuple(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
334
|
+
(
|
|
335
|
+
next(to_dicts(result[self._expression.primary_key]))
|
|
336
|
+
if is_key(attribute)
|
|
337
|
+
else result[attribute][0]
|
|
338
|
+
)
|
|
359
339
|
for attribute in attrs
|
|
360
340
|
)
|
|
361
341
|
ret = return_values[0] if len(attrs) == 1 else return_values
|
datajoint/heading.py
CHANGED
|
@@ -33,6 +33,7 @@ default_attribute_properties = (
|
|
|
33
33
|
is_attachment=False,
|
|
34
34
|
is_filepath=False,
|
|
35
35
|
is_external=False,
|
|
36
|
+
is_hidden=False,
|
|
36
37
|
adapter=None,
|
|
37
38
|
store=None,
|
|
38
39
|
unsupported=False,
|
|
@@ -120,7 +121,7 @@ class Heading:
|
|
|
120
121
|
def attributes(self):
|
|
121
122
|
if self._attributes is None:
|
|
122
123
|
self._init_from_database() # lazy loading from database
|
|
123
|
-
return self._attributes
|
|
124
|
+
return {k: v for k, v in self._attributes.items() if not v.is_hidden}
|
|
124
125
|
|
|
125
126
|
@property
|
|
126
127
|
def names(self):
|
|
@@ -193,10 +194,12 @@ class Heading:
|
|
|
193
194
|
represent heading as the SQL SELECT clause.
|
|
194
195
|
"""
|
|
195
196
|
return ",".join(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
(
|
|
198
|
+
"`%s`" % name
|
|
199
|
+
if self.attributes[name].attribute_expression is None
|
|
200
|
+
else self.attributes[name].attribute_expression
|
|
201
|
+
+ (" as `%s`" % name if include_aliases else "")
|
|
202
|
+
)
|
|
200
203
|
for name in fields
|
|
201
204
|
)
|
|
202
205
|
|
|
@@ -298,6 +301,7 @@ class Heading:
|
|
|
298
301
|
store=None,
|
|
299
302
|
is_external=False,
|
|
300
303
|
attribute_expression=None,
|
|
304
|
+
is_hidden=attr["name"].startswith("_"),
|
|
301
305
|
)
|
|
302
306
|
|
|
303
307
|
if any(TYPE_PATTERN[t].match(attr["type"]) for t in ("INTEGER", "FLOAT")):
|
|
@@ -371,9 +375,11 @@ class Heading:
|
|
|
371
375
|
is_blob=category in ("INTERNAL_BLOB", "EXTERNAL_BLOB"),
|
|
372
376
|
uuid=category == "UUID",
|
|
373
377
|
is_external=category in EXTERNAL_TYPES,
|
|
374
|
-
store=
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
store=(
|
|
379
|
+
attr["type"].split("@")[1]
|
|
380
|
+
if category in EXTERNAL_TYPES
|
|
381
|
+
else None
|
|
382
|
+
),
|
|
377
383
|
)
|
|
378
384
|
|
|
379
385
|
if attr["in_key"] and any(
|
datajoint/preview.py
CHANGED
|
@@ -126,9 +126,9 @@ def repr_html(query_expression):
|
|
|
126
126
|
head_template.format(
|
|
127
127
|
column=c,
|
|
128
128
|
comment=heading.attributes[c].comment,
|
|
129
|
-
primary=
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
primary=(
|
|
130
|
+
"primary" if c in query_expression.primary_key else "nonprimary"
|
|
131
|
+
),
|
|
132
132
|
)
|
|
133
133
|
for c in heading.names
|
|
134
134
|
),
|
|
@@ -145,7 +145,9 @@ def repr_html(query_expression):
|
|
|
145
145
|
for tup in tuples
|
|
146
146
|
]
|
|
147
147
|
),
|
|
148
|
-
count=(
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
count=(
|
|
149
|
+
("<p>Total: %d</p>" % len(rel))
|
|
150
|
+
if config["display.show_tuple_count"]
|
|
151
|
+
else ""
|
|
152
|
+
),
|
|
151
153
|
)
|
datajoint/s3.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
AWS S3 operations
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from io import BytesIO
|
|
5
6
|
import minio # https://docs.minio.io/docs/python-client-api-reference
|
|
6
7
|
import urllib3
|
|
@@ -26,7 +27,7 @@ class Folder:
|
|
|
26
27
|
*,
|
|
27
28
|
secure=False,
|
|
28
29
|
proxy_server=None,
|
|
29
|
-
**_
|
|
30
|
+
**_,
|
|
30
31
|
):
|
|
31
32
|
# from https://docs.min.io/docs/python-client-api-reference
|
|
32
33
|
self.client = minio.Minio(
|
datajoint/schemas.py
CHANGED
|
@@ -2,17 +2,16 @@ import warnings
|
|
|
2
2
|
import logging
|
|
3
3
|
import inspect
|
|
4
4
|
import re
|
|
5
|
-
import itertools
|
|
6
5
|
import collections
|
|
6
|
+
import itertools
|
|
7
7
|
from .connection import conn
|
|
8
|
-
from .diagram import Diagram, _get_tier
|
|
9
8
|
from .settings import config
|
|
10
9
|
from .errors import DataJointError, AccessError
|
|
11
10
|
from .jobs import JobTable
|
|
12
11
|
from .external import ExternalMapping
|
|
13
12
|
from .heading import Heading
|
|
14
13
|
from .utils import user_choice, to_camel_case
|
|
15
|
-
from .user_tables import Part, Computed, Imported, Manual, Lookup
|
|
14
|
+
from .user_tables import Part, Computed, Imported, Manual, Lookup, _get_tier
|
|
16
15
|
from .table import lookup_class_name, Log, FreeTable
|
|
17
16
|
import types
|
|
18
17
|
|
|
@@ -21,7 +20,7 @@ logger = logging.getLogger(__name__.split(".")[0])
|
|
|
21
20
|
|
|
22
21
|
def ordered_dir(class_):
|
|
23
22
|
"""
|
|
24
|
-
List (most) attributes of the class including inherited ones, similar to `dir`
|
|
23
|
+
List (most) attributes of the class including inherited ones, similar to `dir` built-in function,
|
|
25
24
|
but respects order of attribute declaration as much as possible.
|
|
26
25
|
|
|
27
26
|
:param class_: class to list members for
|
|
@@ -413,6 +412,7 @@ class Schema:
|
|
|
413
412
|
|
|
414
413
|
:return: a string containing the body of a complete Python module defining this schema.
|
|
415
414
|
"""
|
|
415
|
+
self.connection.dependencies.load()
|
|
416
416
|
self._assert_exists()
|
|
417
417
|
module_count = itertools.count()
|
|
418
418
|
# add virtual modules for referenced modules with names vmod0, vmod1, ...
|
|
@@ -451,10 +451,8 @@ class Schema:
|
|
|
451
451
|
).replace("\n", "\n " + indent),
|
|
452
452
|
)
|
|
453
453
|
|
|
454
|
-
|
|
455
|
-
body = "\n\n".join(
|
|
456
|
-
make_class_definition(table) for table in diagram.topological_sort()
|
|
457
|
-
)
|
|
454
|
+
tables = self.connection.dependencies.topo_sort()
|
|
455
|
+
body = "\n\n".join(make_class_definition(table) for table in tables)
|
|
458
456
|
python_code = "\n\n".join(
|
|
459
457
|
(
|
|
460
458
|
'"""This module was auto-generated by datajoint from an existing schema"""',
|
|
@@ -480,11 +478,12 @@ class Schema:
|
|
|
480
478
|
|
|
481
479
|
:return: A list of table names from the database schema.
|
|
482
480
|
"""
|
|
481
|
+
self.connection.dependencies.load()
|
|
483
482
|
return [
|
|
484
483
|
t
|
|
485
484
|
for d, t in (
|
|
486
485
|
full_t.replace("`", "").split(".")
|
|
487
|
-
for full_t in
|
|
486
|
+
for full_t in self.connection.dependencies.topo_sort()
|
|
488
487
|
)
|
|
489
488
|
if d == self.database
|
|
490
489
|
]
|
|
@@ -533,7 +532,6 @@ class VirtualModule(types.ModuleType):
|
|
|
533
532
|
|
|
534
533
|
def list_schemas(connection=None):
|
|
535
534
|
"""
|
|
536
|
-
|
|
537
535
|
:param connection: a dj.Connection object
|
|
538
536
|
:return: list of all accessible schemas on the server
|
|
539
537
|
"""
|
datajoint/settings.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Settings for DataJoint.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
@@ -46,6 +47,7 @@ default = dict(
|
|
|
46
47
|
"display.show_tuple_count": True,
|
|
47
48
|
"database.use_tls": None,
|
|
48
49
|
"enable_python_native_blobs": True, # python-native/dj0 encoding support
|
|
50
|
+
"add_hidden_timestamp": False,
|
|
49
51
|
"filepath_checksum_size_limit": None, # file size limit for when to disable checksums
|
|
50
52
|
}
|
|
51
53
|
)
|
|
@@ -245,6 +247,11 @@ class Config(collections.abc.MutableMapping):
|
|
|
245
247
|
self._conf[key] = value
|
|
246
248
|
else:
|
|
247
249
|
raise DataJointError("Validator for {0:s} did not pass".format(key))
|
|
250
|
+
valid_logging_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
251
|
+
if key == "loglevel":
|
|
252
|
+
if value not in valid_logging_levels:
|
|
253
|
+
raise ValueError(f"{'value'} is not a valid logging value")
|
|
254
|
+
logger.setLevel(value)
|
|
248
255
|
|
|
249
256
|
|
|
250
257
|
# Load configuration from file
|
|
@@ -269,6 +276,7 @@ mapping = {
|
|
|
269
276
|
"database.password",
|
|
270
277
|
"external.aws_access_key_id",
|
|
271
278
|
"external.aws_secret_access_key",
|
|
279
|
+
"loglevel",
|
|
272
280
|
),
|
|
273
281
|
map(
|
|
274
282
|
os.getenv,
|
|
@@ -278,6 +286,7 @@ mapping = {
|
|
|
278
286
|
"DJ_PASS",
|
|
279
287
|
"DJ_AWS_ACCESS_KEY_ID",
|
|
280
288
|
"DJ_AWS_SECRET_ACCESS_KEY",
|
|
289
|
+
"DJ_LOG_LEVEL",
|
|
281
290
|
),
|
|
282
291
|
),
|
|
283
292
|
)
|
datajoint/table.py
CHANGED
|
@@ -15,7 +15,7 @@ from .declare import declare, alter
|
|
|
15
15
|
from .condition import make_condition
|
|
16
16
|
from .expression import QueryExpression
|
|
17
17
|
from . import blob
|
|
18
|
-
from .utils import user_choice, get_master
|
|
18
|
+
from .utils import user_choice, get_master, is_camel_case
|
|
19
19
|
from .heading import Heading
|
|
20
20
|
from .errors import (
|
|
21
21
|
DuplicateError,
|
|
@@ -75,6 +75,10 @@ class Table(QueryExpression):
|
|
|
75
75
|
def table_name(self):
|
|
76
76
|
return self._table_name
|
|
77
77
|
|
|
78
|
+
@property
|
|
79
|
+
def class_name(self):
|
|
80
|
+
return self.__class__.__name__
|
|
81
|
+
|
|
78
82
|
@property
|
|
79
83
|
def definition(self):
|
|
80
84
|
raise NotImplementedError(
|
|
@@ -93,6 +97,14 @@ class Table(QueryExpression):
|
|
|
93
97
|
"Cannot declare new tables inside a transaction, "
|
|
94
98
|
"e.g. from inside a populate/make call"
|
|
95
99
|
)
|
|
100
|
+
# Enforce strict CamelCase #1150
|
|
101
|
+
if not is_camel_case(self.class_name):
|
|
102
|
+
raise DataJointError(
|
|
103
|
+
"Table class name `{name}` is invalid. Please use CamelCase. ".format(
|
|
104
|
+
name=self.class_name
|
|
105
|
+
)
|
|
106
|
+
+ "Classes defining tables should be formatted in strict CamelCase."
|
|
107
|
+
)
|
|
96
108
|
sql, external_stores = declare(self.full_table_name, self.definition, context)
|
|
97
109
|
sql = sql.format(database=self.database)
|
|
98
110
|
try:
|
|
@@ -184,7 +196,6 @@ class Table(QueryExpression):
|
|
|
184
196
|
|
|
185
197
|
def children(self, primary=None, as_objects=False, foreign_key_info=False):
|
|
186
198
|
"""
|
|
187
|
-
|
|
188
199
|
:param primary: if None, then all children are returned. If True, then only foreign keys composed of
|
|
189
200
|
primary key attributes are considered. If False, return foreign keys including at least one
|
|
190
201
|
secondary attribute.
|
|
@@ -206,7 +217,6 @@ class Table(QueryExpression):
|
|
|
206
217
|
|
|
207
218
|
def descendants(self, as_objects=False):
|
|
208
219
|
"""
|
|
209
|
-
|
|
210
220
|
:param as_objects: False - a list of table names; True - a list of table objects.
|
|
211
221
|
:return: list of tables descendants in topological order.
|
|
212
222
|
"""
|
|
@@ -218,7 +228,6 @@ class Table(QueryExpression):
|
|
|
218
228
|
|
|
219
229
|
def ancestors(self, as_objects=False):
|
|
220
230
|
"""
|
|
221
|
-
|
|
222
231
|
:param as_objects: False - a list of table names; True - a list of table objects.
|
|
223
232
|
:return: list of tables ancestors in topological order.
|
|
224
233
|
"""
|
|
@@ -230,10 +239,11 @@ class Table(QueryExpression):
|
|
|
230
239
|
|
|
231
240
|
def parts(self, as_objects=False):
|
|
232
241
|
"""
|
|
233
|
-
return part tables either as entries in a dict with foreign key
|
|
242
|
+
return part tables either as entries in a dict with foreign key information or a list of objects
|
|
234
243
|
|
|
235
244
|
:param as_objects: if False (default), the output is a dict describing the foreign keys. If True, return table objects.
|
|
236
245
|
"""
|
|
246
|
+
self.connection.dependencies.load(force=False)
|
|
237
247
|
nodes = [
|
|
238
248
|
node
|
|
239
249
|
for node in self.connection.dependencies.nodes
|
|
@@ -415,7 +425,8 @@ class Table(QueryExpression):
|
|
|
415
425
|
self.connection.query(query)
|
|
416
426
|
return
|
|
417
427
|
|
|
418
|
-
|
|
428
|
+
# collects the field list from first row (passed by reference)
|
|
429
|
+
field_list = []
|
|
419
430
|
rows = list(
|
|
420
431
|
self.__make_row_to_insert(row, field_list, ignore_extra_fields)
|
|
421
432
|
for row in rows
|
|
@@ -474,6 +485,7 @@ class Table(QueryExpression):
|
|
|
474
485
|
transaction: bool = True,
|
|
475
486
|
safemode: Union[bool, None] = None,
|
|
476
487
|
force_parts: bool = False,
|
|
488
|
+
force_masters: bool = False,
|
|
477
489
|
) -> int:
|
|
478
490
|
"""
|
|
479
491
|
Deletes the contents of the table and its dependent tables, recursively.
|
|
@@ -485,6 +497,8 @@ class Table(QueryExpression):
|
|
|
485
497
|
safemode: If `True`, prohibit nested transactions and prompt to confirm. Default
|
|
486
498
|
is `dj.config['safemode']`.
|
|
487
499
|
force_parts: Delete from parts even when not deleting from their masters.
|
|
500
|
+
force_masters: If `True`, include part/master pairs in the cascade.
|
|
501
|
+
Default is `False`.
|
|
488
502
|
|
|
489
503
|
Returns:
|
|
490
504
|
Number of deleted rows (excluding those from dependent tables).
|
|
@@ -495,6 +509,7 @@ class Table(QueryExpression):
|
|
|
495
509
|
DataJointError: Deleting a part table before its master.
|
|
496
510
|
"""
|
|
497
511
|
deleted = set()
|
|
512
|
+
visited_masters = set()
|
|
498
513
|
|
|
499
514
|
def cascade(table):
|
|
500
515
|
"""service function to perform cascading deletes recursively."""
|
|
@@ -504,7 +519,8 @@ class Table(QueryExpression):
|
|
|
504
519
|
delete_count = table.delete_quick(get_count=True)
|
|
505
520
|
except IntegrityError as error:
|
|
506
521
|
match = foreign_key_error_regexp.match(error.args[0]).groupdict()
|
|
507
|
-
|
|
522
|
+
# if schema name missing, use table
|
|
523
|
+
if "`.`" not in match["child"]:
|
|
508
524
|
match["child"] = "{}.{}".format(
|
|
509
525
|
table.full_table_name.split(".")[0], match["child"]
|
|
510
526
|
)
|
|
@@ -547,13 +563,34 @@ class Table(QueryExpression):
|
|
|
547
563
|
and match["fk_attrs"] == match["pk_attrs"]
|
|
548
564
|
):
|
|
549
565
|
child._restriction = table._restriction
|
|
566
|
+
child._restriction_attributes = table.restriction_attributes
|
|
550
567
|
elif match["fk_attrs"] != match["pk_attrs"]:
|
|
551
568
|
child &= table.proj(
|
|
552
569
|
**dict(zip(match["fk_attrs"], match["pk_attrs"]))
|
|
553
570
|
)
|
|
554
571
|
else:
|
|
555
572
|
child &= table.proj()
|
|
556
|
-
|
|
573
|
+
|
|
574
|
+
master_name = get_master(child.full_table_name)
|
|
575
|
+
if (
|
|
576
|
+
force_masters
|
|
577
|
+
and master_name
|
|
578
|
+
and master_name != table.full_table_name
|
|
579
|
+
and master_name not in visited_masters
|
|
580
|
+
):
|
|
581
|
+
master = FreeTable(table.connection, master_name)
|
|
582
|
+
master._restriction_attributes = set()
|
|
583
|
+
master._restriction = [
|
|
584
|
+
make_condition( # &= may cause in target tables in subquery
|
|
585
|
+
master,
|
|
586
|
+
(master.proj() & child.proj()).fetch(),
|
|
587
|
+
master._restriction_attributes,
|
|
588
|
+
)
|
|
589
|
+
]
|
|
590
|
+
visited_masters.add(master_name)
|
|
591
|
+
cascade(master)
|
|
592
|
+
else:
|
|
593
|
+
cascade(child)
|
|
557
594
|
else:
|
|
558
595
|
deleted.add(table.full_table_name)
|
|
559
596
|
logger.info(
|
|
@@ -607,6 +644,8 @@ class Table(QueryExpression):
|
|
|
607
644
|
logger.warn("Nothing to delete.")
|
|
608
645
|
if transaction:
|
|
609
646
|
self.connection.cancel_transaction()
|
|
647
|
+
elif not transaction:
|
|
648
|
+
logger.info("Delete completed")
|
|
610
649
|
else:
|
|
611
650
|
if not safemode or user_choice("Commit deletes?", default="no") == "yes":
|
|
612
651
|
if transaction:
|
|
@@ -758,9 +797,11 @@ class Table(QueryExpression):
|
|
|
758
797
|
if do_include:
|
|
759
798
|
attributes_declared.add(attr.name)
|
|
760
799
|
definition += "%-20s : %-28s %s\n" % (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
800
|
+
(
|
|
801
|
+
attr.name
|
|
802
|
+
if attr.default is None
|
|
803
|
+
else "%s=%s" % (attr.name, attr.default)
|
|
804
|
+
),
|
|
764
805
|
"%s%s"
|
|
765
806
|
% (attr.type, " auto_increment" if attr.autoincrement else ""),
|
|
766
807
|
"# " + attr.comment if attr.comment else "",
|
|
@@ -923,7 +964,8 @@ def lookup_class_name(name, context, depth=3):
|
|
|
923
964
|
while nodes:
|
|
924
965
|
node = nodes.pop(0)
|
|
925
966
|
for member_name, member in node["context"].items():
|
|
926
|
-
|
|
967
|
+
# skip IPython's implicit variables
|
|
968
|
+
if not member_name.startswith("_"):
|
|
927
969
|
if inspect.isclass(member) and issubclass(member, Table):
|
|
928
970
|
if member.full_table_name == name: # found it!
|
|
929
971
|
return ".".join([node["context_name"], member_name]).lstrip(".")
|
datajoint/user_tables.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Hosts the table tiers, user tables should be derived from.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from .table import Table
|
|
6
7
|
from .autopopulate import AutoPopulate
|
|
7
8
|
from .utils import from_camel_case, ClassProperty
|
|
@@ -242,3 +243,29 @@ class Part(UserTable):
|
|
|
242
243
|
def alter(self, prompt=True, context=None):
|
|
243
244
|
# without context, use declaration context which maps master keyword to master table
|
|
244
245
|
super().alter(prompt=prompt, context=context or self.declaration_context)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
user_table_classes = (Manual, Lookup, Computed, Imported, Part)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class _AliasNode:
|
|
252
|
+
"""
|
|
253
|
+
special class to indicate aliased foreign keys
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_tier(table_name):
|
|
260
|
+
"""given the table name, return the user table class."""
|
|
261
|
+
if not table_name.startswith("`"):
|
|
262
|
+
return _AliasNode
|
|
263
|
+
else:
|
|
264
|
+
try:
|
|
265
|
+
return next(
|
|
266
|
+
tier
|
|
267
|
+
for tier in user_table_classes
|
|
268
|
+
if re.fullmatch(tier.tier_regexp, table_name.split("`")[-2])
|
|
269
|
+
)
|
|
270
|
+
except StopIteration:
|
|
271
|
+
return None
|
datajoint/utils.py
CHANGED
|
@@ -53,6 +53,19 @@ def get_master(full_table_name: str) -> str:
|
|
|
53
53
|
return match["master"] + "`" if match else ""
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def is_camel_case(s):
|
|
57
|
+
"""
|
|
58
|
+
Check if a string is in CamelCase notation.
|
|
59
|
+
|
|
60
|
+
:param s: string to check
|
|
61
|
+
:returns: True if the string is in CamelCase notation, False otherwise
|
|
62
|
+
Example:
|
|
63
|
+
>>> is_camel_case("TableName") # returns True
|
|
64
|
+
>>> is_camel_case("table_name") # returns False
|
|
65
|
+
"""
|
|
66
|
+
return bool(re.match(r"^[A-Z][A-Za-z0-9]*$", s))
|
|
67
|
+
|
|
68
|
+
|
|
56
69
|
def to_camel_case(s):
|
|
57
70
|
"""
|
|
58
71
|
Convert names with under score (_) separation into camel case names.
|
|
@@ -82,7 +95,7 @@ def from_camel_case(s):
|
|
|
82
95
|
def convert(match):
|
|
83
96
|
return ("_" if match.groups()[0] else "") + match.group(0).lower()
|
|
84
97
|
|
|
85
|
-
if not
|
|
98
|
+
if not is_camel_case(s):
|
|
86
99
|
raise DataJointError(
|
|
87
100
|
"ClassName must be alphanumeric in CamelCase, begin with a capital letter"
|
|
88
101
|
)
|
datajoint/version.py
CHANGED