datajoint 0.14.2__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 +4 -1
- datajoint/autopopulate.py +9 -2
- datajoint/cli.py +77 -0
- datajoint/condition.py +31 -0
- datajoint/declare.py +20 -3
- datajoint/dependencies.py +64 -32
- datajoint/diagram.py +53 -43
- datajoint/expression.py +82 -33
- datajoint/external.py +7 -2
- datajoint/fetch.py +11 -37
- datajoint/heading.py +3 -1
- datajoint/s3.py +1 -1
- datajoint/schemas.py +7 -9
- datajoint/settings.py +8 -0
- datajoint/table.py +9 -6
- datajoint/user_tables.py +27 -0
- 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.2.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.2.dist-info/METADATA +0 -26
- datajoint-0.14.2.dist-info/RECORD +0 -33
- datajoint-0.14.2.dist-info/datajoint.pub +0 -6
- {datajoint-0.14.2.dist-info → datajoint-0.14.3.dist-info}/LICENSE.txt +0 -0
- {datajoint-0.14.2.dist-info → datajoint-0.14.3.dist-info}/top_level.txt +0 -0
datajoint/expression.py
CHANGED
|
@@ -9,6 +9,7 @@ from .fetch import Fetch, Fetch1
|
|
|
9
9
|
from .preview import preview, repr_html
|
|
10
10
|
from .condition import (
|
|
11
11
|
AndList,
|
|
12
|
+
Top,
|
|
12
13
|
Not,
|
|
13
14
|
make_condition,
|
|
14
15
|
assert_join_compatibility,
|
|
@@ -52,6 +53,7 @@ class QueryExpression:
|
|
|
52
53
|
_connection = None
|
|
53
54
|
_heading = None
|
|
54
55
|
_support = None
|
|
56
|
+
_top = None
|
|
55
57
|
|
|
56
58
|
# If the query will be using distinct
|
|
57
59
|
_distinct = False
|
|
@@ -121,17 +123,33 @@ class QueryExpression:
|
|
|
121
123
|
else " WHERE (%s)" % ")AND(".join(str(s) for s in self.restriction)
|
|
122
124
|
)
|
|
123
125
|
|
|
126
|
+
def sorting_clauses(self):
|
|
127
|
+
if not self._top:
|
|
128
|
+
return ""
|
|
129
|
+
clause = ", ".join(
|
|
130
|
+
_wrap_attributes(
|
|
131
|
+
_flatten_attribute_list(self.primary_key, self._top.order_by)
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
if clause:
|
|
135
|
+
clause = f" ORDER BY {clause}"
|
|
136
|
+
if self._top.limit is not None:
|
|
137
|
+
clause += f" LIMIT {self._top.limit}{f' OFFSET {self._top.offset}' if self._top.offset else ''}"
|
|
138
|
+
|
|
139
|
+
return clause
|
|
140
|
+
|
|
124
141
|
def make_sql(self, fields=None):
|
|
125
142
|
"""
|
|
126
143
|
Make the SQL SELECT statement.
|
|
127
144
|
|
|
128
145
|
:param fields: used to explicitly set the select attributes
|
|
129
146
|
"""
|
|
130
|
-
return "SELECT {distinct}{fields} FROM {from_}{where}".format(
|
|
147
|
+
return "SELECT {distinct}{fields} FROM {from_}{where}{sorting}".format(
|
|
131
148
|
distinct="DISTINCT " if self._distinct else "",
|
|
132
149
|
fields=self.heading.as_sql(fields or self.heading.names),
|
|
133
150
|
from_=self.from_clause(),
|
|
134
151
|
where=self.where_clause(),
|
|
152
|
+
sorting=self.sorting_clauses(),
|
|
135
153
|
)
|
|
136
154
|
|
|
137
155
|
# --------- query operators -----------
|
|
@@ -189,6 +207,14 @@ class QueryExpression:
|
|
|
189
207
|
string, or an AndList.
|
|
190
208
|
"""
|
|
191
209
|
attributes = set()
|
|
210
|
+
if isinstance(restriction, Top):
|
|
211
|
+
result = (
|
|
212
|
+
self.make_subquery()
|
|
213
|
+
if self._top and not self._top.__eq__(restriction)
|
|
214
|
+
else copy.copy(self)
|
|
215
|
+
) # make subquery to avoid overwriting existing Top
|
|
216
|
+
result._top = restriction
|
|
217
|
+
return result
|
|
192
218
|
new_condition = make_condition(self, restriction, attributes)
|
|
193
219
|
if new_condition is True:
|
|
194
220
|
return self # restriction has no effect, return the same object
|
|
@@ -202,8 +228,10 @@ class QueryExpression:
|
|
|
202
228
|
pass # all ok
|
|
203
229
|
# If the new condition uses any new attributes, a subquery is required.
|
|
204
230
|
# However, Aggregation's HAVING statement works fine with aliased attributes.
|
|
205
|
-
need_subquery =
|
|
206
|
-
|
|
231
|
+
need_subquery = (
|
|
232
|
+
isinstance(self, Union)
|
|
233
|
+
or (not isinstance(self, Aggregation) and self.heading.new_attributes)
|
|
234
|
+
or self._top
|
|
207
235
|
)
|
|
208
236
|
if need_subquery:
|
|
209
237
|
result = self.make_subquery()
|
|
@@ -539,19 +567,20 @@ class QueryExpression:
|
|
|
539
567
|
|
|
540
568
|
def __len__(self):
|
|
541
569
|
""":return: number of elements in the result set e.g. ``len(q1)``."""
|
|
542
|
-
|
|
570
|
+
result = self.make_subquery() if self._top else copy.copy(self)
|
|
571
|
+
return result.connection.query(
|
|
543
572
|
"SELECT {select_} FROM {from_}{where}".format(
|
|
544
573
|
select_=(
|
|
545
574
|
"count(*)"
|
|
546
|
-
if any(
|
|
575
|
+
if any(result._left)
|
|
547
576
|
else "count(DISTINCT {fields})".format(
|
|
548
|
-
fields=
|
|
549
|
-
|
|
577
|
+
fields=result.heading.as_sql(
|
|
578
|
+
result.primary_key, include_aliases=False
|
|
550
579
|
)
|
|
551
580
|
)
|
|
552
581
|
),
|
|
553
|
-
from_=
|
|
554
|
-
where=
|
|
582
|
+
from_=result.from_clause(),
|
|
583
|
+
where=result.where_clause(),
|
|
555
584
|
)
|
|
556
585
|
).fetchone()[0]
|
|
557
586
|
|
|
@@ -619,18 +648,12 @@ class QueryExpression:
|
|
|
619
648
|
# -- move on to next entry.
|
|
620
649
|
return next(self)
|
|
621
650
|
|
|
622
|
-
def cursor(self,
|
|
651
|
+
def cursor(self, as_dict=False):
|
|
623
652
|
"""
|
|
624
653
|
See expression.fetch() for input description.
|
|
625
654
|
:return: query cursor
|
|
626
655
|
"""
|
|
627
|
-
if offset and limit is None:
|
|
628
|
-
raise DataJointError("limit is required when offset is set")
|
|
629
656
|
sql = self.make_sql()
|
|
630
|
-
if order_by is not None:
|
|
631
|
-
sql += " ORDER BY " + ", ".join(order_by)
|
|
632
|
-
if limit is not None:
|
|
633
|
-
sql += " LIMIT %d" % limit + (" OFFSET %d" % offset if offset else "")
|
|
634
657
|
logger.debug(sql)
|
|
635
658
|
return self.connection.query(sql, as_dict=as_dict)
|
|
636
659
|
|
|
@@ -701,23 +724,26 @@ class Aggregation(QueryExpression):
|
|
|
701
724
|
fields = self.heading.as_sql(fields or self.heading.names)
|
|
702
725
|
assert self._grouping_attributes or not self.restriction
|
|
703
726
|
distinct = set(self.heading.names) == set(self.primary_key)
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
727
|
+
return (
|
|
728
|
+
"SELECT {distinct}{fields} FROM {from_}{where}{group_by}{sorting}".format(
|
|
729
|
+
distinct="DISTINCT " if distinct else "",
|
|
730
|
+
fields=fields,
|
|
731
|
+
from_=self.from_clause(),
|
|
732
|
+
where=self.where_clause(),
|
|
733
|
+
group_by=(
|
|
734
|
+
""
|
|
735
|
+
if not self.primary_key
|
|
736
|
+
else (
|
|
737
|
+
" GROUP BY `%s`" % "`,`".join(self._grouping_attributes)
|
|
738
|
+
+ (
|
|
739
|
+
""
|
|
740
|
+
if not self.restriction
|
|
741
|
+
else " HAVING (%s)" % ")AND(".join(self.restriction)
|
|
742
|
+
)
|
|
718
743
|
)
|
|
719
|
-
)
|
|
720
|
-
|
|
744
|
+
),
|
|
745
|
+
sorting=self.sorting_clauses(),
|
|
746
|
+
)
|
|
721
747
|
)
|
|
722
748
|
|
|
723
749
|
def __len__(self):
|
|
@@ -776,7 +802,7 @@ class Union(QueryExpression):
|
|
|
776
802
|
):
|
|
777
803
|
# no secondary attributes: use UNION DISTINCT
|
|
778
804
|
fields = arg1.primary_key
|
|
779
|
-
return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}`".format(
|
|
805
|
+
return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}{sorting}`".format(
|
|
780
806
|
sql1=(
|
|
781
807
|
arg1.make_sql()
|
|
782
808
|
if isinstance(arg1, Union)
|
|
@@ -788,6 +814,7 @@ class Union(QueryExpression):
|
|
|
788
814
|
else arg2.make_sql(fields)
|
|
789
815
|
),
|
|
790
816
|
alias=next(self.__count),
|
|
817
|
+
sorting=self.sorting_clauses(),
|
|
791
818
|
)
|
|
792
819
|
# with secondary attributes, use union of left join with antijoin
|
|
793
820
|
fields = self.heading.names
|
|
@@ -939,3 +966,25 @@ class U:
|
|
|
939
966
|
)
|
|
940
967
|
|
|
941
968
|
aggregate = aggr # alias for aggr
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _flatten_attribute_list(primary_key, attrs):
|
|
972
|
+
"""
|
|
973
|
+
:param primary_key: list of attributes in primary key
|
|
974
|
+
:param attrs: list of attribute names, which may include "KEY", "KEY DESC" or "KEY ASC"
|
|
975
|
+
:return: generator of attributes where "KEY" is replaced with its component attributes
|
|
976
|
+
"""
|
|
977
|
+
for a in attrs:
|
|
978
|
+
if re.match(r"^\s*KEY(\s+[aA][Ss][Cc])?\s*$", a):
|
|
979
|
+
if primary_key:
|
|
980
|
+
yield from primary_key
|
|
981
|
+
elif re.match(r"^\s*KEY\s+[Dd][Ee][Ss][Cc]\s*$", a):
|
|
982
|
+
if primary_key:
|
|
983
|
+
yield from (q + " DESC" for q in primary_key)
|
|
984
|
+
else:
|
|
985
|
+
yield a
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _wrap_attributes(attr):
|
|
989
|
+
for entry in attr: # wrap attribute names in backquotes
|
|
990
|
+
yield re.sub(r"\b((?!asc|desc)\w+)\b", r"`\1`", entry, flags=re.IGNORECASE)
|
datajoint/external.py
CHANGED
|
@@ -8,7 +8,7 @@ from .hash import uuid_from_buffer, uuid_from_file
|
|
|
8
8
|
from .table import Table, FreeTable
|
|
9
9
|
from .heading import Heading
|
|
10
10
|
from .declare import EXTERNAL_TABLE_ROOT
|
|
11
|
-
from . import s3
|
|
11
|
+
from . import s3, errors
|
|
12
12
|
from .utils import safe_write, safe_copy
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
@@ -141,7 +141,12 @@ class ExternalTable(Table):
|
|
|
141
141
|
if self.spec["protocol"] == "s3":
|
|
142
142
|
return self.s3.get(external_path)
|
|
143
143
|
if self.spec["protocol"] == "file":
|
|
144
|
-
|
|
144
|
+
try:
|
|
145
|
+
return Path(external_path).read_bytes()
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
raise errors.MissingExternalFile(
|
|
148
|
+
f"Missing external file {external_path}"
|
|
149
|
+
) from None
|
|
145
150
|
assert False
|
|
146
151
|
|
|
147
152
|
def _remove_external_file(self, external_path):
|
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,
|
|
@@ -257,9 +233,7 @@ class Fetch:
|
|
|
257
233
|
]
|
|
258
234
|
ret = return_values[0] if len(attrs) == 1 else return_values
|
|
259
235
|
else: # fetch all attributes as a numpy.record_array or pandas.DataFrame
|
|
260
|
-
cur = self._expression.cursor(
|
|
261
|
-
as_dict=as_dict, limit=limit, offset=offset, order_by=order_by
|
|
262
|
-
)
|
|
236
|
+
cur = self._expression.cursor(as_dict=as_dict)
|
|
263
237
|
heading = self._expression.heading
|
|
264
238
|
if as_dict:
|
|
265
239
|
ret = [
|
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):
|
|
@@ -300,6 +301,7 @@ class Heading:
|
|
|
300
301
|
store=None,
|
|
301
302
|
is_external=False,
|
|
302
303
|
attribute_expression=None,
|
|
304
|
+
is_hidden=attr["name"].startswith("_"),
|
|
303
305
|
)
|
|
304
306
|
|
|
305
307
|
if any(TYPE_PATTERN[t].match(attr["type"]) for t in ("INTEGER", "FLOAT")):
|
datajoint/s3.py
CHANGED
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
|
|
|
@@ -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
|
@@ -47,6 +47,7 @@ default = dict(
|
|
|
47
47
|
"display.show_tuple_count": True,
|
|
48
48
|
"database.use_tls": None,
|
|
49
49
|
"enable_python_native_blobs": True, # python-native/dj0 encoding support
|
|
50
|
+
"add_hidden_timestamp": False,
|
|
50
51
|
"filepath_checksum_size_limit": None, # file size limit for when to disable checksums
|
|
51
52
|
}
|
|
52
53
|
)
|
|
@@ -246,6 +247,11 @@ class Config(collections.abc.MutableMapping):
|
|
|
246
247
|
self._conf[key] = value
|
|
247
248
|
else:
|
|
248
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)
|
|
249
255
|
|
|
250
256
|
|
|
251
257
|
# Load configuration from file
|
|
@@ -270,6 +276,7 @@ mapping = {
|
|
|
270
276
|
"database.password",
|
|
271
277
|
"external.aws_access_key_id",
|
|
272
278
|
"external.aws_secret_access_key",
|
|
279
|
+
"loglevel",
|
|
273
280
|
),
|
|
274
281
|
map(
|
|
275
282
|
os.getenv,
|
|
@@ -279,6 +286,7 @@ mapping = {
|
|
|
279
286
|
"DJ_PASS",
|
|
280
287
|
"DJ_AWS_ACCESS_KEY_ID",
|
|
281
288
|
"DJ_AWS_SECRET_ACCESS_KEY",
|
|
289
|
+
"DJ_LOG_LEVEL",
|
|
282
290
|
),
|
|
283
291
|
),
|
|
284
292
|
)
|
datajoint/table.py
CHANGED
|
@@ -196,7 +196,6 @@ class Table(QueryExpression):
|
|
|
196
196
|
|
|
197
197
|
def children(self, primary=None, as_objects=False, foreign_key_info=False):
|
|
198
198
|
"""
|
|
199
|
-
|
|
200
199
|
:param primary: if None, then all children are returned. If True, then only foreign keys composed of
|
|
201
200
|
primary key attributes are considered. If False, return foreign keys including at least one
|
|
202
201
|
secondary attribute.
|
|
@@ -218,7 +217,6 @@ class Table(QueryExpression):
|
|
|
218
217
|
|
|
219
218
|
def descendants(self, as_objects=False):
|
|
220
219
|
"""
|
|
221
|
-
|
|
222
220
|
:param as_objects: False - a list of table names; True - a list of table objects.
|
|
223
221
|
:return: list of tables descendants in topological order.
|
|
224
222
|
"""
|
|
@@ -230,7 +228,6 @@ class Table(QueryExpression):
|
|
|
230
228
|
|
|
231
229
|
def ancestors(self, as_objects=False):
|
|
232
230
|
"""
|
|
233
|
-
|
|
234
231
|
:param as_objects: False - a list of table names; True - a list of table objects.
|
|
235
232
|
:return: list of tables ancestors in topological order.
|
|
236
233
|
"""
|
|
@@ -246,6 +243,7 @@ class Table(QueryExpression):
|
|
|
246
243
|
|
|
247
244
|
:param as_objects: if False (default), the output is a dict describing the foreign keys. If True, return table objects.
|
|
248
245
|
"""
|
|
246
|
+
self.connection.dependencies.load(force=False)
|
|
249
247
|
nodes = [
|
|
250
248
|
node
|
|
251
249
|
for node in self.connection.dependencies.nodes
|
|
@@ -427,7 +425,8 @@ class Table(QueryExpression):
|
|
|
427
425
|
self.connection.query(query)
|
|
428
426
|
return
|
|
429
427
|
|
|
430
|
-
|
|
428
|
+
# collects the field list from first row (passed by reference)
|
|
429
|
+
field_list = []
|
|
431
430
|
rows = list(
|
|
432
431
|
self.__make_row_to_insert(row, field_list, ignore_extra_fields)
|
|
433
432
|
for row in rows
|
|
@@ -520,7 +519,8 @@ class Table(QueryExpression):
|
|
|
520
519
|
delete_count = table.delete_quick(get_count=True)
|
|
521
520
|
except IntegrityError as error:
|
|
522
521
|
match = foreign_key_error_regexp.match(error.args[0]).groupdict()
|
|
523
|
-
|
|
522
|
+
# if schema name missing, use table
|
|
523
|
+
if "`.`" not in match["child"]:
|
|
524
524
|
match["child"] = "{}.{}".format(
|
|
525
525
|
table.full_table_name.split(".")[0], match["child"]
|
|
526
526
|
)
|
|
@@ -644,6 +644,8 @@ class Table(QueryExpression):
|
|
|
644
644
|
logger.warn("Nothing to delete.")
|
|
645
645
|
if transaction:
|
|
646
646
|
self.connection.cancel_transaction()
|
|
647
|
+
elif not transaction:
|
|
648
|
+
logger.info("Delete completed")
|
|
647
649
|
else:
|
|
648
650
|
if not safemode or user_choice("Commit deletes?", default="no") == "yes":
|
|
649
651
|
if transaction:
|
|
@@ -962,7 +964,8 @@ def lookup_class_name(name, context, depth=3):
|
|
|
962
964
|
while nodes:
|
|
963
965
|
node = nodes.pop(0)
|
|
964
966
|
for member_name, member in node["context"].items():
|
|
965
|
-
|
|
967
|
+
# skip IPython's implicit variables
|
|
968
|
+
if not member_name.startswith("_"):
|
|
966
969
|
if inspect.isclass(member) and issubclass(member, Table):
|
|
967
970
|
if member.full_table_name == name: # found it!
|
|
968
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/version.py
CHANGED