Skip to content

Commit d981561

Browse files
Merge remote-tracking branch 'refs/remotes/origin/master'
2 parents cb3600b + bae66d2 commit d981561

31 files changed

+960
-152
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ FROM mcr.microsoft.com/devcontainers/python:${PY_VER}-${DISTRO}
44
RUN \
55
apt update && \
66
apt-get install bash-completion graphviz default-mysql-client -y && \
7-
pip install flake8 black faker ipykernel pytest pytest-cov nose nose-cov datajoint && \
7+
pip install flake8 black faker ipykernel pytest pytest-cov nose nose-cov datajoint jupyterlab && \
88
pip uninstall datajoint -y
99

1010
USER root

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"features": {
3333
"ghcr.io/devcontainers/features/git:1": {},
3434
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
35+
"ghcr.io/devcontainers/features/github-cli:1": {},
3536
},
3637
// Configure tool-specific properties.
3738
"customizations": {

.github/workflows/development.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ jobs:
190190
prerelease: false
191191
draft: false
192192
- name: Fetch pip artifacts
193-
uses: actions/download-artifact@v3
193+
uses: actions/download-artifact@v4.1.7
194194
with:
195195
name: pip-datajoint-${{env.DJ_VERSION}}
196196
path: dist

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"editor.formatOnPaste": false,
3-
"editor.formatOnSave": true,
3+
"editor.formatOnSave": false,
44
"editor.rulers": [
55
94
66
],

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
## Release notes
22

33
### 0.14.3 -- TBD
4+
- Added - `dj.Top` restriction ([#1024](https://github.com/datajoint/datajoint-python/issues/1024)) PR [#1084](https://github.com/datajoint/datajoint-python/pull/1084)
45
- Fixed - Added encapsulating double quotes to comply with [DOT language](https://graphviz.org/doc/info/lang.html) - PR [#1177](https://github.com/datajoint/datajoint-python/pull/1177)
6+
- Added - Datajoint python CLI ([#940](https://github.com/datajoint/datajoint-python/issues/940)) PR [#1095](https://github.com/datajoint/datajoint-python/pull/1095)
7+
- Added - Ability to set hidden attributes on a table - PR [#1091](https://github.com/datajoint/datajoint-python/pull/1091)
8+
- Added - Ability to specify a list of keys to popuate - PR [#989](https://github.com/datajoint/datajoint-python/pull/989)
59

610
### 0.14.2 -- Aug 19, 2024
711
- Added - Migrate nosetests to pytest - PR [#1142](https://github.com/datajoint/datajoint-python/pull/1142)

datajoint/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"Part",
3838
"Not",
3939
"AndList",
40+
"Top",
4041
"U",
4142
"Diagram",
4243
"Di",
@@ -51,6 +52,7 @@
5152
"key",
5253
"key_hash",
5354
"logger",
55+
"cli",
5456
]
5557

5658
from .logging import logger
@@ -61,7 +63,7 @@
6163
from .schemas import VirtualModule, list_schemas
6264
from .table import Table, FreeTable
6365
from .user_tables import Manual, Lookup, Imported, Computed, Part
64-
from .expression import Not, AndList, U
66+
from .expression import Not, AndList, U, Top
6567
from .diagram import Diagram
6668
from .admin import set_password, kill
6769
from .blob import MatCell, MatStruct
@@ -70,6 +72,7 @@
7072
from .attribute_adapter import AttributeAdapter
7173
from . import errors
7274
from .errors import DataJointError
75+
from .cli import cli
7376

7477
ERD = Di = Diagram # Aliases for Diagram
7578
schema = Schema # Aliases for Schema

datajoint/autopopulate.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
def _initialize_populate(table, jobs, populate_kwargs):
2525
"""
26-
Initialize the process for mulitprocessing.
26+
Initialize the process for multiprocessing.
2727
Saves the unpickled copy of the table to the current process and reconnects.
2828
"""
2929
process = mp.current_process()
@@ -153,6 +153,7 @@ def _jobs_to_do(self, restrictions):
153153
def populate(
154154
self,
155155
*restrictions,
156+
keys=None,
156157
suppress_errors=False,
157158
return_exception_objects=False,
158159
reserve_jobs=False,
@@ -169,6 +170,8 @@ def populate(
169170
170171
:param restrictions: a list of restrictions each restrict
171172
(table.key_source - target.proj())
173+
:param keys: The list of keys (dicts) to send to self.make().
174+
If None (default), then use self.key_source to query they keys.
172175
:param suppress_errors: if True, do not terminate execution.
173176
:param return_exception_objects: return error objects instead of just error messages
174177
:param reserve_jobs: if True, reserve jobs to populate in asynchronous fashion
@@ -206,7 +209,10 @@ def handler(signum, frame):
206209

207210
old_handler = signal.signal(signal.SIGTERM, handler)
208211

209-
keys = (self._jobs_to_do(restrictions) - self.target).fetch("KEY", limit=limit)
212+
if keys is None:
213+
keys = (self._jobs_to_do(restrictions) - self.target).fetch(
214+
"KEY", limit=limit
215+
)
210216

211217
# exclude "error", "ignore" or "reserved" jobs
212218
if reserve_jobs:
@@ -295,6 +301,7 @@ def _populate1(
295301
:return: (key, error) when suppress_errors=True,
296302
True if successfully invoke one `make()` call, otherwise False
297303
"""
304+
# use the legacy `_make_tuples` callback.
298305
make = self._make_tuples if hasattr(self, "_make_tuples") else self.make
299306

300307
if jobs is not None and not jobs.reserve(

datajoint/cli.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import argparse
2+
from code import interact
3+
from collections import ChainMap
4+
import datajoint as dj
5+
6+
7+
def cli(args: list = None):
8+
"""
9+
Console interface for DataJoint Python
10+
11+
:param args: List of arguments to be passed in, defaults to reading stdin
12+
:type args: list, optional
13+
"""
14+
parser = argparse.ArgumentParser(
15+
prog="datajoint",
16+
description="DataJoint console interface.",
17+
conflict_handler="resolve",
18+
)
19+
parser.add_argument(
20+
"-V", "--version", action="version", version=f"{dj.__name__} {dj.__version__}"
21+
)
22+
parser.add_argument(
23+
"-u",
24+
"--user",
25+
type=str,
26+
default=dj.config["database.user"],
27+
required=False,
28+
help="Datajoint username",
29+
)
30+
parser.add_argument(
31+
"-p",
32+
"--password",
33+
type=str,
34+
default=dj.config["database.password"],
35+
required=False,
36+
help="Datajoint password",
37+
)
38+
parser.add_argument(
39+
"-h",
40+
"--host",
41+
type=str,
42+
default=dj.config["database.host"],
43+
required=False,
44+
help="Datajoint host",
45+
)
46+
parser.add_argument(
47+
"-s",
48+
"--schemas",
49+
nargs="+",
50+
type=str,
51+
required=False,
52+
help="A list of virtual module mappings in `db:schema ...` format",
53+
)
54+
kwargs = vars(parser.parse_args(args))
55+
mods = {}
56+
if kwargs["user"]:
57+
dj.config["database.user"] = kwargs["user"]
58+
if kwargs["password"]:
59+
dj.config["database.password"] = kwargs["password"]
60+
if kwargs["host"]:
61+
dj.config["database.host"] = kwargs["host"]
62+
if kwargs["schemas"]:
63+
for vm in kwargs["schemas"]:
64+
d, m = vm.split(":")
65+
mods[m] = dj.create_virtual_module(m, d)
66+
67+
banner = "dj repl\n"
68+
if mods:
69+
modstr = "\n".join(" - {}".format(m) for m in mods)
70+
banner += "\nschema modules:\n\n" + modstr + "\n"
71+
interact(banner, local=dict(ChainMap(mods, locals(), globals())))
72+
73+
raise SystemExit
74+
75+
76+
if __name__ == "__main__":
77+
cli()

datajoint/condition.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import pandas
1111
import json
1212
from .errors import DataJointError
13+
from typing import Union, List
14+
from dataclasses import dataclass
1315

1416
JSON_PATTERN = re.compile(
1517
r"^(?P<attr>\w+)(\.(?P<path>[\w.*\[\]]+))?(:(?P<type>[\w(,\s)]+))?$"
@@ -61,6 +63,35 @@ def append(self, restriction):
6163
super().append(restriction)
6264

6365

66+
@dataclass
67+
class Top:
68+
"""
69+
A restriction to the top entities of a query.
70+
In SQL, this corresponds to ORDER BY ... LIMIT ... OFFSET
71+
"""
72+
73+
limit: Union[int, None] = 1
74+
order_by: Union[str, List[str]] = "KEY"
75+
offset: int = 0
76+
77+
def __post_init__(self):
78+
self.order_by = self.order_by or ["KEY"]
79+
self.offset = self.offset or 0
80+
81+
if self.limit is not None and not isinstance(self.limit, int):
82+
raise TypeError("Top limit must be an integer")
83+
if not isinstance(self.order_by, (str, collections.abc.Sequence)) or not all(
84+
isinstance(r, str) for r in self.order_by
85+
):
86+
raise TypeError("Top order_by attributes must all be strings")
87+
if not isinstance(self.offset, int):
88+
raise TypeError("The offset argument must be an integer")
89+
if self.offset and self.limit is None:
90+
self.limit = 999999999999 # arbitrary large number to allow query
91+
if isinstance(self.order_by, str):
92+
self.order_by = [self.order_by]
93+
94+
6495
class Not:
6596
"""invert restriction"""
6697

datajoint/declare.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
import pyparsing as pp
88
import logging
9+
from hashlib import sha1
910
from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
1011
from .attribute_adapter import get_adapter
1112
from .condition import translate_attribute
@@ -310,6 +311,18 @@ def declare(full_table_name, definition, context):
310311
external_stores,
311312
) = prepare_declare(definition, context)
312313

314+
metadata_attr_sql = [
315+
"`_{full_table_name}_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP"
316+
]
317+
attribute_sql.extend(
318+
attr.format(
319+
full_table_name=sha1(
320+
full_table_name.replace("`", "").encode("utf-8")
321+
).hexdigest()
322+
)
323+
for attr in metadata_attr_sql
324+
)
325+
313326
if not primary_key:
314327
raise DataJointError("Table must have a primary key")
315328

@@ -442,9 +455,11 @@ def format_attribute(attr):
442455
return f"`{attr}`"
443456
return f"({attr})"
444457

445-
match = re.match(
446-
r"(?P<unique>unique\s+)?index\s*\(\s*(?P<args>.*)\)", line, re.I
447-
).groupdict()
458+
match = re.match(r"(?P<unique>unique\s+)?index\s*\(\s*(?P<args>.*)\)", line, re.I)
459+
if match is None:
460+
raise DataJointError(f'Table definition syntax error in line "{line}"')
461+
match = match.groupdict()
462+
448463
attr_list = re.findall(r"(?:[^,(]|\([^)]*\))+", match["args"])
449464
index_sql.append(
450465
"{unique}index ({attrs})".format(

0 commit comments

Comments
 (0)