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/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 = isinstance(self, Union) or (
206
- not isinstance(self, Aggregation) and self.heading.new_attributes
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
- return self.connection.query(
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(self._left)
575
+ if any(result._left)
547
576
  else "count(DISTINCT {fields})".format(
548
- fields=self.heading.as_sql(
549
- self.primary_key, include_aliases=False
577
+ fields=result.heading.as_sql(
578
+ result.primary_key, include_aliases=False
550
579
  )
551
580
  )
552
581
  ),
553
- from_=self.from_clause(),
554
- where=self.where_clause(),
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, offset=0, limit=None, order_by=None, as_dict=False):
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 "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)
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
- return Path(external_path).read_bytes()
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 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)
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
@@ -27,7 +27,7 @@ class Folder:
27
27
  *,
28
28
  secure=False,
29
29
  proxy_server=None,
30
- **_
30
+ **_,
31
31
  ):
32
32
  # from https://docs.min.io/docs/python-client-api-reference
33
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
 
@@ -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
- diagram = Diagram(self)
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 Diagram(self).topological_sort()
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
- field_list = [] # collects the field list from first row (passed by reference)
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
- if "`.`" not in match["child"]: # if schema name missing, use table
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
- if not member_name.startswith("_"): # skip IPython's implicit variables
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
@@ -1,3 +1,3 @@
1
- __version__ = "0.14.2"
1
+ __version__ = "0.14.3"
2
2
 
3
3
  assert len(__version__) <= 10 # The log table limits version to the 10 characters