datajoint 0.14.2__py3-none-any.whl → 0.14.4__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 +16 -14
- datajoint/admin.py +4 -2
- datajoint/attribute_adapter.py +1 -0
- datajoint/autopopulate.py +62 -20
- datajoint/blob.py +6 -5
- datajoint/cli.py +78 -0
- datajoint/condition.py +38 -5
- datajoint/connection.py +17 -10
- datajoint/declare.py +25 -6
- datajoint/dependencies.py +67 -33
- datajoint/diagram.py +58 -48
- datajoint/expression.py +92 -42
- datajoint/external.py +17 -10
- datajoint/fetch.py +18 -42
- datajoint/hash.py +1 -1
- datajoint/heading.py +14 -11
- datajoint/jobs.py +4 -3
- datajoint/plugin.py +5 -3
- datajoint/s3.py +6 -4
- datajoint/schemas.py +18 -19
- datajoint/settings.py +25 -11
- datajoint/table.py +27 -22
- datajoint/user_tables.py +30 -2
- datajoint/utils.py +2 -1
- datajoint/version.py +4 -1
- datajoint-0.14.4.dist-info/METADATA +703 -0
- datajoint-0.14.4.dist-info/RECORD +34 -0
- {datajoint-0.14.2.dist-info → datajoint-0.14.4.dist-info}/WHEEL +1 -1
- datajoint-0.14.4.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.4.dist-info/licenses}/LICENSE.txt +0 -0
- {datajoint-0.14.2.dist-info → datajoint-0.14.4.dist-info}/top_level.txt +0 -0
datajoint/diagram.py
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import networkx as nx
|
|
2
|
-
import re
|
|
3
1
|
import functools
|
|
2
|
+
import inspect
|
|
4
3
|
import io
|
|
5
4
|
import logging
|
|
6
|
-
import inspect
|
|
7
|
-
from .table import Table
|
|
8
|
-
from .dependencies import unite_master_parts
|
|
9
|
-
from .user_tables import Manual, Imported, Computed, Lookup, Part
|
|
10
|
-
from .errors import DataJointError
|
|
11
|
-
from .table import lookup_class_name
|
|
12
5
|
|
|
6
|
+
import networkx as nx
|
|
7
|
+
|
|
8
|
+
from .dependencies import topo_sort
|
|
9
|
+
from .errors import DataJointError
|
|
10
|
+
from .table import Table, lookup_class_name
|
|
11
|
+
from .user_tables import Computed, Imported, Lookup, Manual, Part, _AliasNode, _get_tier
|
|
13
12
|
|
|
14
13
|
try:
|
|
15
14
|
from matplotlib import pyplot as plt
|
|
@@ -27,29 +26,6 @@ except:
|
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
30
|
-
user_table_classes = (Manual, Lookup, Computed, Imported, Part)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class _AliasNode:
|
|
34
|
-
"""
|
|
35
|
-
special class to indicate aliased foreign keys
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _get_tier(table_name):
|
|
42
|
-
if not table_name.startswith("`"):
|
|
43
|
-
return _AliasNode
|
|
44
|
-
else:
|
|
45
|
-
try:
|
|
46
|
-
return next(
|
|
47
|
-
tier
|
|
48
|
-
for tier in user_table_classes
|
|
49
|
-
if re.fullmatch(tier.tier_regexp, table_name.split("`")[-2])
|
|
50
|
-
)
|
|
51
|
-
except StopIteration:
|
|
52
|
-
return None
|
|
53
29
|
|
|
54
30
|
|
|
55
31
|
if not diagram_active:
|
|
@@ -59,8 +35,7 @@ if not diagram_active:
|
|
|
59
35
|
Entity relationship diagram, currently disabled due to the lack of required packages: matplotlib and pygraphviz.
|
|
60
36
|
|
|
61
37
|
To enable Diagram feature, please install both matplotlib and pygraphviz. For instructions on how to install
|
|
62
|
-
these two packages, refer to
|
|
63
|
-
http://tutorials.datajoint.io/setting-up/datajoint-python.html
|
|
38
|
+
these two packages, refer to https://datajoint.com/docs/core/datajoint-python/0.14/client/install/
|
|
64
39
|
"""
|
|
65
40
|
|
|
66
41
|
def __init__(self, *args, **kwargs):
|
|
@@ -72,19 +47,22 @@ else:
|
|
|
72
47
|
|
|
73
48
|
class Diagram(nx.DiGraph):
|
|
74
49
|
"""
|
|
75
|
-
|
|
50
|
+
Schema diagram showing tables and foreign keys between in the form of a directed
|
|
51
|
+
acyclic graph (DAG). The diagram is derived from the connection.dependencies object.
|
|
76
52
|
|
|
77
53
|
Usage:
|
|
78
54
|
|
|
79
55
|
>>> diag = Diagram(source)
|
|
80
56
|
|
|
81
|
-
source can be a
|
|
57
|
+
source can be a table object, a table class, a schema, or a module that has a schema.
|
|
82
58
|
|
|
83
59
|
>>> diag.draw()
|
|
84
60
|
|
|
85
61
|
draws the diagram using pyplot
|
|
86
62
|
|
|
87
63
|
diag1 + diag2 - combines the two diagrams.
|
|
64
|
+
diag1 - diag2 - difference between diagrams
|
|
65
|
+
diag1 * diag2 - intersection of diagrams
|
|
88
66
|
diag + n - expands n levels of successors
|
|
89
67
|
diag - n - expands n levels of predecessors
|
|
90
68
|
Thus dj.Diagram(schema.Table)+1-1 defines the diagram of immediate ancestors and descendants of schema.Table
|
|
@@ -94,6 +72,7 @@ else:
|
|
|
94
72
|
"""
|
|
95
73
|
|
|
96
74
|
def __init__(self, source, context=None):
|
|
75
|
+
|
|
97
76
|
if isinstance(source, Diagram):
|
|
98
77
|
# copy constructor
|
|
99
78
|
self.nodes_to_show = set(source.nodes_to_show)
|
|
@@ -154,7 +133,7 @@ else:
|
|
|
154
133
|
|
|
155
134
|
def add_parts(self):
|
|
156
135
|
"""
|
|
157
|
-
Adds to the diagram the part tables of tables already
|
|
136
|
+
Adds to the diagram the part tables of all master tables already in the diagram
|
|
158
137
|
:return:
|
|
159
138
|
"""
|
|
160
139
|
|
|
@@ -179,16 +158,6 @@ else:
|
|
|
179
158
|
)
|
|
180
159
|
return self
|
|
181
160
|
|
|
182
|
-
def topological_sort(self):
|
|
183
|
-
""":return: list of nodes in topological order"""
|
|
184
|
-
return unite_master_parts(
|
|
185
|
-
list(
|
|
186
|
-
nx.algorithms.dag.topological_sort(
|
|
187
|
-
nx.DiGraph(self).subgraph(self.nodes_to_show)
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
|
|
192
161
|
def __add__(self, arg):
|
|
193
162
|
"""
|
|
194
163
|
:param arg: either another Diagram or a positive integer.
|
|
@@ -256,6 +225,10 @@ else:
|
|
|
256
225
|
self.nodes_to_show.intersection_update(arg.nodes_to_show)
|
|
257
226
|
return self
|
|
258
227
|
|
|
228
|
+
def topo_sort(self):
|
|
229
|
+
"""return nodes in lexicographical topological order"""
|
|
230
|
+
return topo_sort(self)
|
|
231
|
+
|
|
259
232
|
def _make_graph(self):
|
|
260
233
|
"""
|
|
261
234
|
Make the self.graph - a graph object ready for drawing
|
|
@@ -300,6 +273,36 @@ else:
|
|
|
300
273
|
nx.relabel_nodes(graph, mapping, copy=False)
|
|
301
274
|
return graph
|
|
302
275
|
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _encapsulate_edge_attributes(graph):
|
|
278
|
+
"""
|
|
279
|
+
Modifies the `nx.Graph`'s edge attribute `attr_map` to be a string representation
|
|
280
|
+
of the attribute map, and encapsulates the string in double quotes.
|
|
281
|
+
Changes the graph in place.
|
|
282
|
+
|
|
283
|
+
Implements workaround described in
|
|
284
|
+
https://github.com/pydot/pydot/issues/258#issuecomment-795798099
|
|
285
|
+
"""
|
|
286
|
+
for u, v, *_, edgedata in graph.edges(data=True):
|
|
287
|
+
if "attr_map" in edgedata:
|
|
288
|
+
graph.edges[u, v]["attr_map"] = '"{0}"'.format(edgedata["attr_map"])
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _encapsulate_node_names(graph):
|
|
292
|
+
"""
|
|
293
|
+
Modifies the `nx.Graph`'s node names string representations encapsulated in
|
|
294
|
+
double quotes.
|
|
295
|
+
Changes the graph in place.
|
|
296
|
+
|
|
297
|
+
Implements workaround described in
|
|
298
|
+
https://github.com/datajoint/datajoint-python/pull/1176
|
|
299
|
+
"""
|
|
300
|
+
nx.relabel_nodes(
|
|
301
|
+
graph,
|
|
302
|
+
{node: '"{0}"'.format(node) for node in graph.nodes()},
|
|
303
|
+
copy=False,
|
|
304
|
+
)
|
|
305
|
+
|
|
303
306
|
def make_dot(self):
|
|
304
307
|
graph = self._make_graph()
|
|
305
308
|
graph.nodes()
|
|
@@ -368,6 +371,8 @@ else:
|
|
|
368
371
|
for node, d in dict(graph.nodes(data=True)).items()
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
self._encapsulate_node_names(graph)
|
|
375
|
+
self._encapsulate_edge_attributes(graph)
|
|
371
376
|
dot = nx.drawing.nx_pydot.to_pydot(graph)
|
|
372
377
|
for node in dot.get_nodes():
|
|
373
378
|
node.set_shape("circle")
|
|
@@ -408,9 +413,14 @@ else:
|
|
|
408
413
|
|
|
409
414
|
for edge in dot.get_edges():
|
|
410
415
|
# see https://graphviz.org/doc/info/attrs.html
|
|
411
|
-
src = edge.get_source()
|
|
412
|
-
dest = edge.get_destination()
|
|
416
|
+
src = edge.get_source()
|
|
417
|
+
dest = edge.get_destination()
|
|
413
418
|
props = graph.get_edge_data(src, dest)
|
|
419
|
+
if props is None:
|
|
420
|
+
raise DataJointError(
|
|
421
|
+
"Could not find edge with source "
|
|
422
|
+
"'{}' and destination '{}'".format(src, dest)
|
|
423
|
+
)
|
|
414
424
|
edge.set_color("#00000040")
|
|
415
425
|
edge.set_style("solid" if props["primary"] else "dashed")
|
|
416
426
|
master_part = graph.nodes[dest][
|
datajoint/expression.py
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
from itertools import count
|
|
2
|
-
import logging
|
|
3
|
-
import inspect
|
|
4
1
|
import copy
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
5
4
|
import re
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from .fetch import Fetch, Fetch1
|
|
9
|
-
from .preview import preview, repr_html
|
|
5
|
+
from itertools import count
|
|
6
|
+
|
|
10
7
|
from .condition import (
|
|
11
8
|
AndList,
|
|
12
9
|
Not,
|
|
13
|
-
|
|
10
|
+
PromiscuousOperand,
|
|
11
|
+
Top,
|
|
14
12
|
assert_join_compatibility,
|
|
15
13
|
extract_column_names,
|
|
16
|
-
|
|
14
|
+
make_condition,
|
|
17
15
|
translate_attribute,
|
|
18
16
|
)
|
|
19
17
|
from .declare import CONSTANT_LITERALS
|
|
18
|
+
from .errors import DataJointError
|
|
19
|
+
from .fetch import Fetch, Fetch1
|
|
20
|
+
from .preview import preview, repr_html
|
|
21
|
+
from .settings import config
|
|
20
22
|
|
|
21
23
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
22
24
|
|
|
@@ -52,6 +54,7 @@ class QueryExpression:
|
|
|
52
54
|
_connection = None
|
|
53
55
|
_heading = None
|
|
54
56
|
_support = None
|
|
57
|
+
_top = None
|
|
55
58
|
|
|
56
59
|
# If the query will be using distinct
|
|
57
60
|
_distinct = False
|
|
@@ -121,17 +124,33 @@ class QueryExpression:
|
|
|
121
124
|
else " WHERE (%s)" % ")AND(".join(str(s) for s in self.restriction)
|
|
122
125
|
)
|
|
123
126
|
|
|
127
|
+
def sorting_clauses(self):
|
|
128
|
+
if not self._top:
|
|
129
|
+
return ""
|
|
130
|
+
clause = ", ".join(
|
|
131
|
+
_wrap_attributes(
|
|
132
|
+
_flatten_attribute_list(self.primary_key, self._top.order_by)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
if clause:
|
|
136
|
+
clause = f" ORDER BY {clause}"
|
|
137
|
+
if self._top.limit is not None:
|
|
138
|
+
clause += f" LIMIT {self._top.limit}{f' OFFSET {self._top.offset}' if self._top.offset else ''}"
|
|
139
|
+
|
|
140
|
+
return clause
|
|
141
|
+
|
|
124
142
|
def make_sql(self, fields=None):
|
|
125
143
|
"""
|
|
126
144
|
Make the SQL SELECT statement.
|
|
127
145
|
|
|
128
146
|
:param fields: used to explicitly set the select attributes
|
|
129
147
|
"""
|
|
130
|
-
return "SELECT {distinct}{fields} FROM {from_}{where}".format(
|
|
148
|
+
return "SELECT {distinct}{fields} FROM {from_}{where}{sorting}".format(
|
|
131
149
|
distinct="DISTINCT " if self._distinct else "",
|
|
132
150
|
fields=self.heading.as_sql(fields or self.heading.names),
|
|
133
151
|
from_=self.from_clause(),
|
|
134
152
|
where=self.where_clause(),
|
|
153
|
+
sorting=self.sorting_clauses(),
|
|
135
154
|
)
|
|
136
155
|
|
|
137
156
|
# --------- query operators -----------
|
|
@@ -189,6 +208,14 @@ class QueryExpression:
|
|
|
189
208
|
string, or an AndList.
|
|
190
209
|
"""
|
|
191
210
|
attributes = set()
|
|
211
|
+
if isinstance(restriction, Top):
|
|
212
|
+
result = (
|
|
213
|
+
self.make_subquery()
|
|
214
|
+
if self._top and not self._top.__eq__(restriction)
|
|
215
|
+
else copy.copy(self)
|
|
216
|
+
) # make subquery to avoid overwriting existing Top
|
|
217
|
+
result._top = restriction
|
|
218
|
+
return result
|
|
192
219
|
new_condition = make_condition(self, restriction, attributes)
|
|
193
220
|
if new_condition is True:
|
|
194
221
|
return self # restriction has no effect, return the same object
|
|
@@ -202,8 +229,10 @@ class QueryExpression:
|
|
|
202
229
|
pass # all ok
|
|
203
230
|
# If the new condition uses any new attributes, a subquery is required.
|
|
204
231
|
# However, Aggregation's HAVING statement works fine with aliased attributes.
|
|
205
|
-
need_subquery =
|
|
206
|
-
|
|
232
|
+
need_subquery = (
|
|
233
|
+
isinstance(self, Union)
|
|
234
|
+
or (not isinstance(self, Aggregation) and self.heading.new_attributes)
|
|
235
|
+
or self._top
|
|
207
236
|
)
|
|
208
237
|
if need_subquery:
|
|
209
238
|
result = self.make_subquery()
|
|
@@ -539,19 +568,20 @@ class QueryExpression:
|
|
|
539
568
|
|
|
540
569
|
def __len__(self):
|
|
541
570
|
""":return: number of elements in the result set e.g. ``len(q1)``."""
|
|
542
|
-
|
|
571
|
+
result = self.make_subquery() if self._top else copy.copy(self)
|
|
572
|
+
return result.connection.query(
|
|
543
573
|
"SELECT {select_} FROM {from_}{where}".format(
|
|
544
574
|
select_=(
|
|
545
575
|
"count(*)"
|
|
546
|
-
if any(
|
|
576
|
+
if any(result._left)
|
|
547
577
|
else "count(DISTINCT {fields})".format(
|
|
548
|
-
fields=
|
|
549
|
-
|
|
578
|
+
fields=result.heading.as_sql(
|
|
579
|
+
result.primary_key, include_aliases=False
|
|
550
580
|
)
|
|
551
581
|
)
|
|
552
582
|
),
|
|
553
|
-
from_=
|
|
554
|
-
where=
|
|
583
|
+
from_=result.from_clause(),
|
|
584
|
+
where=result.where_clause(),
|
|
555
585
|
)
|
|
556
586
|
).fetchone()[0]
|
|
557
587
|
|
|
@@ -619,18 +649,12 @@ class QueryExpression:
|
|
|
619
649
|
# -- move on to next entry.
|
|
620
650
|
return next(self)
|
|
621
651
|
|
|
622
|
-
def cursor(self,
|
|
652
|
+
def cursor(self, as_dict=False):
|
|
623
653
|
"""
|
|
624
654
|
See expression.fetch() for input description.
|
|
625
655
|
:return: query cursor
|
|
626
656
|
"""
|
|
627
|
-
if offset and limit is None:
|
|
628
|
-
raise DataJointError("limit is required when offset is set")
|
|
629
657
|
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
658
|
logger.debug(sql)
|
|
635
659
|
return self.connection.query(sql, as_dict=as_dict)
|
|
636
660
|
|
|
@@ -701,23 +725,26 @@ class Aggregation(QueryExpression):
|
|
|
701
725
|
fields = self.heading.as_sql(fields or self.heading.names)
|
|
702
726
|
assert self._grouping_attributes or not self.restriction
|
|
703
727
|
distinct = set(self.heading.names) == set(self.primary_key)
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
728
|
+
return (
|
|
729
|
+
"SELECT {distinct}{fields} FROM {from_}{where}{group_by}{sorting}".format(
|
|
730
|
+
distinct="DISTINCT " if distinct else "",
|
|
731
|
+
fields=fields,
|
|
732
|
+
from_=self.from_clause(),
|
|
733
|
+
where=self.where_clause(),
|
|
734
|
+
group_by=(
|
|
735
|
+
""
|
|
736
|
+
if not self.primary_key
|
|
737
|
+
else (
|
|
738
|
+
" GROUP BY `%s`" % "`,`".join(self._grouping_attributes)
|
|
739
|
+
+ (
|
|
740
|
+
""
|
|
741
|
+
if not self.restriction
|
|
742
|
+
else " HAVING (%s)" % ")AND(".join(self.restriction)
|
|
743
|
+
)
|
|
718
744
|
)
|
|
719
|
-
)
|
|
720
|
-
|
|
745
|
+
),
|
|
746
|
+
sorting=self.sorting_clauses(),
|
|
747
|
+
)
|
|
721
748
|
)
|
|
722
749
|
|
|
723
750
|
def __len__(self):
|
|
@@ -776,7 +803,7 @@ class Union(QueryExpression):
|
|
|
776
803
|
):
|
|
777
804
|
# no secondary attributes: use UNION DISTINCT
|
|
778
805
|
fields = arg1.primary_key
|
|
779
|
-
return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}`".format(
|
|
806
|
+
return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}{sorting}`".format(
|
|
780
807
|
sql1=(
|
|
781
808
|
arg1.make_sql()
|
|
782
809
|
if isinstance(arg1, Union)
|
|
@@ -788,6 +815,7 @@ class Union(QueryExpression):
|
|
|
788
815
|
else arg2.make_sql(fields)
|
|
789
816
|
),
|
|
790
817
|
alias=next(self.__count),
|
|
818
|
+
sorting=self.sorting_clauses(),
|
|
791
819
|
)
|
|
792
820
|
# with secondary attributes, use union of left join with antijoin
|
|
793
821
|
fields = self.heading.names
|
|
@@ -939,3 +967,25 @@ class U:
|
|
|
939
967
|
)
|
|
940
968
|
|
|
941
969
|
aggregate = aggr # alias for aggr
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _flatten_attribute_list(primary_key, attrs):
|
|
973
|
+
"""
|
|
974
|
+
:param primary_key: list of attributes in primary key
|
|
975
|
+
:param attrs: list of attribute names, which may include "KEY", "KEY DESC" or "KEY ASC"
|
|
976
|
+
:return: generator of attributes where "KEY" is replaced with its component attributes
|
|
977
|
+
"""
|
|
978
|
+
for a in attrs:
|
|
979
|
+
if re.match(r"^\s*KEY(\s+[aA][Ss][Cc])?\s*$", a):
|
|
980
|
+
if primary_key:
|
|
981
|
+
yield from primary_key
|
|
982
|
+
elif re.match(r"^\s*KEY\s+[Dd][Ee][Ss][Cc]\s*$", a):
|
|
983
|
+
if primary_key:
|
|
984
|
+
yield from (q + " DESC" for q in primary_key)
|
|
985
|
+
else:
|
|
986
|
+
yield a
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _wrap_attributes(attr):
|
|
990
|
+
for entry in attr: # wrap attribute names in backquotes
|
|
991
|
+
yield re.sub(r"\b((?!asc|desc)\w+)\b", r"`\1`", entry, flags=re.IGNORECASE)
|
datajoint/external.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
from collections.abc import Mapping
|
|
3
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
4
|
+
|
|
3
5
|
from tqdm import tqdm
|
|
4
|
-
|
|
5
|
-
from .
|
|
6
|
+
|
|
7
|
+
from . import errors, s3
|
|
8
|
+
from .declare import EXTERNAL_TABLE_ROOT
|
|
6
9
|
from .errors import DataJointError, MissingExternalFile
|
|
7
10
|
from .hash import uuid_from_buffer, uuid_from_file
|
|
8
|
-
from .table import Table, FreeTable
|
|
9
11
|
from .heading import Heading
|
|
10
|
-
from .
|
|
11
|
-
from . import
|
|
12
|
-
from .utils import
|
|
12
|
+
from .settings import config
|
|
13
|
+
from .table import FreeTable, Table
|
|
14
|
+
from .utils import safe_copy, safe_write
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
15
17
|
|
|
@@ -22,7 +24,7 @@ SUPPORT_MIGRATED_BLOBS = True # support blobs migrated from datajoint 0.11.*
|
|
|
22
24
|
|
|
23
25
|
def subfold(name, folds):
|
|
24
26
|
"""
|
|
25
|
-
subfolding for external storage:
|
|
27
|
+
subfolding for external storage: e.g. subfold('aBCdefg', (2, 3)) --> ['ab','cde']
|
|
26
28
|
"""
|
|
27
29
|
return (
|
|
28
30
|
(name[: folds[0]].lower(),) + subfold(name[folds[0] :], folds[1:])
|
|
@@ -141,7 +143,12 @@ class ExternalTable(Table):
|
|
|
141
143
|
if self.spec["protocol"] == "s3":
|
|
142
144
|
return self.s3.get(external_path)
|
|
143
145
|
if self.spec["protocol"] == "file":
|
|
144
|
-
|
|
146
|
+
try:
|
|
147
|
+
return Path(external_path).read_bytes()
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
raise errors.MissingExternalFile(
|
|
150
|
+
f"Missing external file {external_path}"
|
|
151
|
+
) from None
|
|
145
152
|
assert False
|
|
146
153
|
|
|
147
154
|
def _remove_external_file(self, external_path):
|
|
@@ -273,7 +280,7 @@ class ExternalTable(Table):
|
|
|
273
280
|
|
|
274
281
|
# check if the remote file already exists and verify that it matches
|
|
275
282
|
check_hash = (self & {"hash": uuid}).fetch("contents_hash")
|
|
276
|
-
if check_hash:
|
|
283
|
+
if check_hash.size:
|
|
277
284
|
# the tracking entry exists, check that it's the same file as before
|
|
278
285
|
if contents_hash != check_hash[0]:
|
|
279
286
|
raise DataJointError(
|
datajoint/fetch.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
from functools import partial
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import logging
|
|
4
|
-
import pandas
|
|
5
1
|
import itertools
|
|
6
|
-
import re
|
|
7
2
|
import json
|
|
8
|
-
import numpy as np
|
|
9
|
-
import uuid
|
|
10
3
|
import numbers
|
|
4
|
+
import uuid
|
|
5
|
+
from functools import partial
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas
|
|
10
|
+
|
|
11
|
+
from datajoint.condition import Top
|
|
12
|
+
|
|
11
13
|
from . import blob, hash
|
|
12
14
|
from .errors import DataJointError
|
|
13
15
|
from .settings import config
|
|
14
16
|
from .utils import safe_write
|
|
15
17
|
|
|
16
|
-
logger = logging.getLogger(__name__.split(".")[0])
|
|
17
|
-
|
|
18
18
|
|
|
19
19
|
class key:
|
|
20
20
|
"""
|
|
@@ -119,21 +119,6 @@ def _get(connection, attr, data, squeeze, download_path):
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
|
|
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
122
|
class Fetch:
|
|
138
123
|
"""
|
|
139
124
|
A fetch object that handles retrieving elements from the table expression.
|
|
@@ -153,7 +138,7 @@ class Fetch:
|
|
|
153
138
|
format=None,
|
|
154
139
|
as_dict=None,
|
|
155
140
|
squeeze=False,
|
|
156
|
-
download_path="."
|
|
141
|
+
download_path=".",
|
|
157
142
|
):
|
|
158
143
|
"""
|
|
159
144
|
Fetches the expression results from the database into an np.array or list of dictionaries and
|
|
@@ -174,13 +159,13 @@ class Fetch:
|
|
|
174
159
|
:param download_path: for fetches that download data, e.g. attachments
|
|
175
160
|
:return: the contents of the table in the form of a structured numpy.array or a dict list
|
|
176
161
|
"""
|
|
177
|
-
if order_by
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
if offset or order_by or limit:
|
|
163
|
+
self._expression = self._expression.restrict(
|
|
164
|
+
Top(
|
|
165
|
+
limit,
|
|
166
|
+
order_by,
|
|
167
|
+
offset,
|
|
168
|
+
)
|
|
184
169
|
)
|
|
185
170
|
|
|
186
171
|
attrs_as_dict = as_dict and attrs
|
|
@@ -212,13 +197,6 @@ class Fetch:
|
|
|
212
197
|
'use "array" or "frame"'.format(format)
|
|
213
198
|
)
|
|
214
199
|
|
|
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
200
|
get = partial(
|
|
223
201
|
_get,
|
|
224
202
|
self._expression.connection,
|
|
@@ -257,9 +235,7 @@ class Fetch:
|
|
|
257
235
|
]
|
|
258
236
|
ret = return_values[0] if len(attrs) == 1 else return_values
|
|
259
237
|
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
|
-
)
|
|
238
|
+
cur = self._expression.cursor(as_dict=as_dict)
|
|
263
239
|
heading = self._expression.heading
|
|
264
240
|
if as_dict:
|
|
265
241
|
ret = [
|
datajoint/hash.py
CHANGED
datajoint/heading.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from collections import namedtuple, defaultdict
|
|
3
|
-
from itertools import chain
|
|
4
|
-
import re
|
|
5
1
|
import logging
|
|
6
|
-
|
|
2
|
+
import re
|
|
3
|
+
from collections import defaultdict, namedtuple
|
|
4
|
+
from itertools import chain
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .attribute_adapter import AttributeAdapter, get_adapter
|
|
7
9
|
from .declare import (
|
|
8
|
-
UUID_DATA_TYPE,
|
|
9
|
-
SPECIAL_TYPES,
|
|
10
|
-
TYPE_PATTERN,
|
|
11
10
|
EXTERNAL_TYPES,
|
|
12
11
|
NATIVE_TYPES,
|
|
12
|
+
SPECIAL_TYPES,
|
|
13
|
+
TYPE_PATTERN,
|
|
14
|
+
UUID_DATA_TYPE,
|
|
13
15
|
)
|
|
14
|
-
from .
|
|
15
|
-
|
|
16
|
+
from .errors import FILEPATH_FEATURE_SWITCH, DataJointError, _support_filepath_types
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
18
19
|
|
|
@@ -33,6 +34,7 @@ default_attribute_properties = (
|
|
|
33
34
|
is_attachment=False,
|
|
34
35
|
is_filepath=False,
|
|
35
36
|
is_external=False,
|
|
37
|
+
is_hidden=False,
|
|
36
38
|
adapter=None,
|
|
37
39
|
store=None,
|
|
38
40
|
unsupported=False,
|
|
@@ -120,7 +122,7 @@ class Heading:
|
|
|
120
122
|
def attributes(self):
|
|
121
123
|
if self._attributes is None:
|
|
122
124
|
self._init_from_database() # lazy loading from database
|
|
123
|
-
return self._attributes
|
|
125
|
+
return {k: v for k, v in self._attributes.items() if not v.is_hidden}
|
|
124
126
|
|
|
125
127
|
@property
|
|
126
128
|
def names(self):
|
|
@@ -300,6 +302,7 @@ class Heading:
|
|
|
300
302
|
store=None,
|
|
301
303
|
is_external=False,
|
|
302
304
|
attribute_expression=None,
|
|
305
|
+
is_hidden=attr["name"].startswith("_"),
|
|
303
306
|
)
|
|
304
307
|
|
|
305
308
|
if any(TYPE_PATTERN[t].match(attr["type"]) for t in ("INTEGER", "FLOAT")):
|
datajoint/jobs.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from .hash import key_hash
|
|
3
2
|
import platform
|
|
4
|
-
|
|
5
|
-
from .settings import config
|
|
3
|
+
|
|
6
4
|
from .errors import DuplicateError
|
|
5
|
+
from .hash import key_hash
|
|
7
6
|
from .heading import Heading
|
|
7
|
+
from .settings import config
|
|
8
|
+
from .table import Table
|
|
8
9
|
|
|
9
10
|
ERROR_MESSAGE_LENGTH = 2047
|
|
10
11
|
TRUNCATION_APPENDIX = "...truncated"
|
datajoint/plugin.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import pkg_resources
|
|
1
|
+
import logging
|
|
3
2
|
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pkg_resources
|
|
4
5
|
from cryptography.exceptions import InvalidSignature
|
|
5
6
|
from otumat import hash_pkg, verify
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
from .settings import config
|
|
7
9
|
|
|
8
10
|
logger = logging.getLogger(__name__.split(".")[0])
|
|
9
11
|
|