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/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 http://docs.datajoint.io/setup/Install-and-connect.html#python and
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
- Entity relationship diagram.
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 base table object, a base table class, a schema, or a module that has a schema.
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 included in the diagram
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().strip('"')
412
- dest = edge.get_destination().strip('"')
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 .settings import config
7
- from .errors import DataJointError
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
- make_condition,
10
+ PromiscuousOperand,
11
+ Top,
14
12
  assert_join_compatibility,
15
13
  extract_column_names,
16
- PromiscuousOperand,
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 = isinstance(self, Union) or (
206
- not isinstance(self, Aggregation) and self.heading.new_attributes
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
- return self.connection.query(
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(self._left)
576
+ if any(result._left)
547
577
  else "count(DISTINCT {fields})".format(
548
- fields=self.heading.as_sql(
549
- self.primary_key, include_aliases=False
578
+ fields=result.heading.as_sql(
579
+ result.primary_key, include_aliases=False
550
580
  )
551
581
  )
552
582
  ),
553
- from_=self.from_clause(),
554
- where=self.where_clause(),
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, offset=0, limit=None, order_by=None, as_dict=False):
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 "SELECT {distinct}{fields} FROM {from_}{where}{group_by}".format(
705
- distinct="DISTINCT " if distinct else "",
706
- fields=fields,
707
- from_=self.from_clause(),
708
- where=self.where_clause(),
709
- group_by=(
710
- ""
711
- if not self.primary_key
712
- else (
713
- " GROUP BY `%s`" % "`,`".join(self._grouping_attributes)
714
- + (
715
- ""
716
- if not self.restriction
717
- else " HAVING (%s)" % ")AND(".join(self.restriction)
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
- from pathlib import Path, PurePosixPath, PureWindowsPath
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
- import logging
5
- from .settings import config
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 .declare import EXTERNAL_TABLE_ROOT
11
- from . import s3
12
- from .utils import safe_write, safe_copy
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: e.g. subfold('aBCdefg', (2, 3)) --> ['ab','cde']
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
- return Path(external_path).read_bytes()
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 is not None:
178
- # if 'order_by' passed in a string, make into list
179
- if isinstance(order_by, str):
180
- order_by = [order_by]
181
- # expand "KEY" or "KEY DESC"
182
- order_by = list(
183
- _flatten_attribute_list(self._expression.primary_key, order_by)
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
@@ -1,6 +1,6 @@
1
1
  import hashlib
2
- import uuid
3
2
  import io
3
+ import uuid
4
4
  from pathlib import Path
5
5
 
6
6
 
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
- from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
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 .attribute_adapter import get_adapter, AttributeAdapter
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
- from .table import Table
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
- from .settings import config
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
- import logging
7
+
8
+ from .settings import config
7
9
 
8
10
  logger = logging.getLogger(__name__.split(".")[0])
9
11