datajoint 0.14.1__py3-none-any.whl → 0.14.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of datajoint might be problematic. Click here for more details.

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