labdata 0.1.22__tar.gz → 0.1.24__tar.gz
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.
- {labdata-0.1.22 → labdata-0.1.24}/PKG-INFO +1 -1
- {labdata-0.1.22 → labdata-0.1.24}/labdata/__init__.py +2 -1
- {labdata-0.1.22 → labdata-0.1.24}/labdata/accounts.py +57 -3
- {labdata-0.1.22 → labdata-0.1.24}/labdata/cli.py +436 -5
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/__init__.py +2 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/ec2.py +4 -4
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/ephys.py +308 -126
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/pcluster.py +8 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/schedulers.py +3 -1
- labdata-0.1.24/labdata/compute/spikeinterface.py +937 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/utils.py +47 -24
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/compute.py +1 -1
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/drift.py +12 -16
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/sorting.py +31 -18
- labdata-0.1.24/labdata/export/__init__.py +15 -0
- labdata-0.1.24/labdata/export/database.py +198 -0
- labdata-0.1.24/labdata/export/docker/Dockerfile.in +37 -0
- labdata-0.1.24/labdata/export/docker/entrypoint.sh +75 -0
- labdata-0.1.24/labdata/export/docker/load_database.sh +25 -0
- labdata-0.1.24/labdata/export/docker/mysql.cnf +4 -0
- labdata-0.1.24/labdata/export/docker.py +319 -0
- labdata-0.1.24/labdata/export/selection.py +1034 -0
- labdata-0.1.24/labdata/export/share.py +197 -0
- labdata-0.1.24/labdata/export/users.py +160 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/rules/ephys.py +241 -6
- {labdata-0.1.22 → labdata-0.1.24}/labdata/rules/utils.py +18 -12
- {labdata-0.1.22 → labdata-0.1.24}/labdata/s3.py +296 -139
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/ephys.py +253 -73
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/general.py +4 -3
- {labdata-0.1.22 → labdata-0.1.24}/labdata/utils.py +69 -8
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/PKG-INFO +1 -1
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/SOURCES.txt +11 -1
- {labdata-0.1.22 → labdata-0.1.24}/pyproject.toml +4 -1
- labdata-0.1.22/labdata/export.py +0 -381
- {labdata-0.1.22 → labdata-0.1.24}/LICENSE +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/README.md +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/caiman.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/pose.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/singularity.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/submission.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/compute/suite2p.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/copy.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/activity.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/admin.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/ai_utils.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/console.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/decisiontask.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/docstrings.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/ephys.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/events.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/imaging.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/index.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/launch.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/local_files.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/notes.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/procedures.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/raster.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/reads.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/schema_graph.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/segmentation.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/sync.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/upload.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/upload_worker.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/video.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/dashboard/writes.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/ephys_io.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/ephys_signal.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/rules/__init__.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/rules/imaging.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/__init__.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/histology.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/onephoton.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/procedures.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/tasks.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/twophoton.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/utils.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/schema/video.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/stacks.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata/widgets.py +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/requires.txt +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.1.22 → labdata-0.1.24}/setup.cfg +0 -0
|
@@ -78,13 +78,22 @@ def database_grants(name, projects, database_name='lab_data', sandbox=False):
|
|
|
78
78
|
return [g.replace('%', '%%') for g in grants]
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def split_host(host, port=3306):
|
|
82
|
+
'''Return ``(address, port)`` of a database host written ``address`` or ``address:port``;
|
|
83
|
+
``port`` is used when the host has none.'''
|
|
84
|
+
address, _, number = str(host).strip().rpartition(':')
|
|
85
|
+
if address and number.isdigit():
|
|
86
|
+
return address, int(number)
|
|
87
|
+
return str(host).strip(), int(port)
|
|
88
|
+
|
|
89
|
+
|
|
81
90
|
def ask_database_admin(host, default_user=None):
|
|
82
91
|
'''Ask for the database admin login and return a pymysql connection.
|
|
83
92
|
|
|
84
93
|
Parameters
|
|
85
94
|
----------
|
|
86
95
|
host : str
|
|
87
|
-
Database address
|
|
96
|
+
Database address, ``address`` or ``address:port``.
|
|
88
97
|
default_user : str, optional
|
|
89
98
|
User offered as default.
|
|
90
99
|
|
|
@@ -96,7 +105,8 @@ def ask_database_admin(host, default_user=None):
|
|
|
96
105
|
print(f'Database admin login for {host} (used for this command only, not saved).')
|
|
97
106
|
user = input(f' Admin user [{default_user or ""}]: ').strip() or default_user
|
|
98
107
|
password = getpass(' Admin password: ')
|
|
99
|
-
|
|
108
|
+
address, port = split_host(host)
|
|
109
|
+
return pymysql.connect(host=address, port=port, user=user, password=password, autocommit=True)
|
|
100
110
|
|
|
101
111
|
|
|
102
112
|
def database_user_exists(conn, name):
|
|
@@ -147,6 +157,49 @@ def database_user_grants(conn, name):
|
|
|
147
157
|
return [r[0] for r in _rows(conn, f"SHOW GRANTS FOR '{name}'@'%%'", ())]
|
|
148
158
|
|
|
149
159
|
|
|
160
|
+
def _grant_pattern(schema):
|
|
161
|
+
'''Return the regular expression of a schema name of a GRANT, where ``_`` and ``%`` are
|
|
162
|
+
wildcards unless escaped with ``\\``.'''
|
|
163
|
+
out, i = '', 0
|
|
164
|
+
while i < len(schema):
|
|
165
|
+
c = schema[i]
|
|
166
|
+
if c == '\\' and i + 1 < len(schema):
|
|
167
|
+
out += re.escape(schema[i + 1])
|
|
168
|
+
i += 2
|
|
169
|
+
continue
|
|
170
|
+
out += '.*' if c == '%' else '.' if c == '_' else re.escape(c)
|
|
171
|
+
i += 1
|
|
172
|
+
return re.compile(out)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def database_access(conn, databases):
|
|
176
|
+
'''Return ``{user: [databases]}`` for the database users with privileges other than
|
|
177
|
+
REFERENCES on ``databases``; ``'*'`` stands for a grant on every database. Users whose
|
|
178
|
+
grants can not be read are left out.'''
|
|
179
|
+
access = dict()
|
|
180
|
+
for user in list_database_users(conn):
|
|
181
|
+
try:
|
|
182
|
+
grants = database_user_grants(conn, user)
|
|
183
|
+
except Exception:
|
|
184
|
+
continue
|
|
185
|
+
for grant in grants:
|
|
186
|
+
m = re.match(r'GRANT (.+?) ON (\S+?)\.\S+ TO ', grant)
|
|
187
|
+
if not m:
|
|
188
|
+
continue
|
|
189
|
+
privileges = set(p.strip().upper() for p in m.group(1).split(','))
|
|
190
|
+
if privileges <= {'USAGE', 'REFERENCES'}:
|
|
191
|
+
continue
|
|
192
|
+
schema = m.group(2).strip('`')
|
|
193
|
+
if schema == '*':
|
|
194
|
+
access.setdefault(user, set()).add('*')
|
|
195
|
+
continue
|
|
196
|
+
pattern = _grant_pattern(schema)
|
|
197
|
+
for database in databases:
|
|
198
|
+
if pattern.fullmatch(database):
|
|
199
|
+
access.setdefault(user, set()).add(database)
|
|
200
|
+
return {user: sorted(names) for user, names in sorted(access.items())}
|
|
201
|
+
|
|
202
|
+
|
|
150
203
|
def create_s3_user(session, name, groups=(), policies=(), like=None):
|
|
151
204
|
'''Create the IAM user ``name`` with one access key, and the permissions of ``groups``, ``policies`` or user ``like``.
|
|
152
205
|
|
|
@@ -836,7 +889,8 @@ def grant_database_project(conn, name, project, database_name='lab_data'):
|
|
|
836
889
|
def connect_database_admin(host, user, password):
|
|
837
890
|
'''Return a pymysql connection for ``user``, after checking it may create users and grant permissions.'''
|
|
838
891
|
import pymysql
|
|
839
|
-
|
|
892
|
+
address, port = split_host(host)
|
|
893
|
+
conn = pymysql.connect(host=address, port=port, user=user, password=password, autocommit=True)
|
|
840
894
|
grants = ' '.join(r[0] for r in _rows(conn, 'SHOW GRANTS', ())).upper()
|
|
841
895
|
if 'CREATE USER' not in grants and 'ALL PRIVILEGES ON *.*' not in grants:
|
|
842
896
|
conn.close()
|
|
@@ -41,6 +41,10 @@ class CLI_parser():
|
|
|
41
41
|
Create and manage the database and S3 accounts of lab members (admin use).
|
|
42
42
|
access
|
|
43
43
|
Manage what the members of a project can read in S3 (admin use).
|
|
44
|
+
share
|
|
45
|
+
Copy tables, with their parents and part tables, from one project to another (admin use).
|
|
46
|
+
export
|
|
47
|
+
Copy a selection to its own database, a dump folder or a Docker image (admin use).
|
|
44
48
|
'''
|
|
45
49
|
def __init__(self):
|
|
46
50
|
parser = argparse.ArgumentParser(
|
|
@@ -74,6 +78,8 @@ Administration commands (don't run on experimental computers):
|
|
|
74
78
|
{tcolor["r"]("cluster")} create|status <name> Creates or checks the AWS ParallelCluster
|
|
75
79
|
{tcolor["r"]("account")} create|show|list|grant|revoke <name> Creates and manages database and S3 accounts
|
|
76
80
|
{tcolor["r"]("access")} show|sync|share <project> Sets what a project's members can read in S3
|
|
81
|
+
{tcolor["r"]("share")} <table> --from <project> --to <project> Shares tables with another project of the lab
|
|
82
|
+
{tcolor["r"]("export")} <table> --docker <folder> Exports data to a database, dump or Docker image
|
|
77
83
|
|
|
78
84
|
''')
|
|
79
85
|
|
|
@@ -615,7 +621,9 @@ Administration commands (don't run on experimental computers):
|
|
|
615
621
|
whose output goes to ``task_log`` and ``~/.labdata/logs``,
|
|
616
622
|
instead of in the terminal.
|
|
617
623
|
--queue
|
|
618
|
-
Print the
|
|
624
|
+
Print the scheduler queue of each target, then the ``ComputeTask``
|
|
625
|
+
table by status with the last line of each task log, and exit.
|
|
626
|
+
With an analysis name, only the tasks of that analysis.
|
|
619
627
|
--delete-computes
|
|
620
628
|
Delete all ``ComputeTask`` entries for this analysis (careful).
|
|
621
629
|
--
|
|
@@ -666,6 +674,8 @@ Available analysis are
|
|
|
666
674
|
targets = np.unique([prefs['compute']['default_target']]+
|
|
667
675
|
list(prefs['compute']['remotes'].keys()))
|
|
668
676
|
for q in targets:
|
|
677
|
+
if q == 'local': # no scheduler queue, the tasks are in the table below
|
|
678
|
+
continue
|
|
669
679
|
queue = check_queue(target = q)
|
|
670
680
|
if queue is None:
|
|
671
681
|
continue
|
|
@@ -678,7 +688,14 @@ Available analysis are
|
|
|
678
688
|
cc = tcolor['g']
|
|
679
689
|
else:
|
|
680
690
|
cc = tcolor['r']
|
|
681
|
-
print(f"\t {j['jobid']} {j['name']} {cc('
|
|
691
|
+
print(f"\t {j['jobid']} {j['name']} {cc(j['state'].lower())} time: {tcolor['y'](j['time'])}\n\t\tpartition: {j['partition']} node: {j['nodelist(reason)']} ")
|
|
692
|
+
# the ComputeTask table, optionally for one analysis: labdata run <analysis> --queue
|
|
693
|
+
qparser = argparse.ArgumentParser(add_help = False)
|
|
694
|
+
qparser.add_argument('analysis', nargs = '?', default = None)
|
|
695
|
+
qparser.add_argument('-p','--project', default = _get_project())
|
|
696
|
+
qargs, _ = qparser.parse_known_args(argum)
|
|
697
|
+
from .utils import load_project_schema
|
|
698
|
+
self._print_compute_tasks(load_project_schema(qargs.project), analysis = qargs.analysis)
|
|
682
699
|
return
|
|
683
700
|
args = parser.parse_args(argum)
|
|
684
701
|
if '--delete-computes' in argum:
|
|
@@ -1220,6 +1237,9 @@ examples:
|
|
|
1220
1237
|
--s3-like, --s3-group, --s3-policy
|
|
1221
1238
|
Extra IAM permissions: copy the groups and policies of a user, or
|
|
1222
1239
|
add groups and managed policies (names or ARNs).
|
|
1240
|
+
--database-name
|
|
1241
|
+
Lab database (``database.name``) of the projects, for a copy made with
|
|
1242
|
+
``labdata share --database-name``. Default: the one of the preference file.
|
|
1223
1243
|
--profile, --region
|
|
1224
1244
|
AWS admin profile and region (asked when omitted).
|
|
1225
1245
|
--dry-run
|
|
@@ -1246,6 +1266,7 @@ examples:
|
|
|
1246
1266
|
labdata account create
|
|
1247
1267
|
labdata account create <name> -p <project> --restore --no-sandbox
|
|
1248
1268
|
labdata account grant <name> -p <other project>
|
|
1269
|
+
labdata account grant <name> -p other_project --database-name newlab # a copy in another lab database
|
|
1249
1270
|
labdata account show <name>
|
|
1250
1271
|
''')
|
|
1251
1272
|
parser.add_argument('action', action = 'store',
|
|
@@ -1276,13 +1297,17 @@ examples:
|
|
|
1276
1297
|
help = 'AWS profile with rights to manage IAM (asked when omitted).')
|
|
1277
1298
|
parser.add_argument('--region', action = 'store', default = None, type = str,
|
|
1278
1299
|
help = 'AWS region (asked when omitted).')
|
|
1300
|
+
parser.add_argument('--database-name', action = 'store', default = None, type = str,
|
|
1301
|
+
help = 'Lab database (database.name) of the projects, e.g. a copy made with '
|
|
1302
|
+
'"labdata share --database-name" (default: the one of the preference file).')
|
|
1279
1303
|
parser.add_argument('--dry-run', action = 'store_true', default = False,
|
|
1280
1304
|
help = 'Print the plan and change nothing.')
|
|
1281
1305
|
args = parser.parse_args(sys.argv[2:])
|
|
1282
1306
|
|
|
1283
1307
|
from . import accounts
|
|
1284
1308
|
host = prefs['database'].get('database.host')
|
|
1285
|
-
database_name = prefs['database'].get('database.name', 'lab_data')
|
|
1309
|
+
database_name = args.database_name or prefs['database'].get('database.name', 'lab_data')
|
|
1310
|
+
self._account_database_name = database_name
|
|
1286
1311
|
admin_user = prefs['database'].get('database.user')
|
|
1287
1312
|
|
|
1288
1313
|
if args.action == 'list':
|
|
@@ -1756,9 +1781,13 @@ examples:
|
|
|
1756
1781
|
def _write_account_file(self, name, projects, password, keys):
|
|
1757
1782
|
'''Write the preference file of a user with their new credentials.'''
|
|
1758
1783
|
from . import accounts
|
|
1784
|
+
base = dict(prefs)
|
|
1785
|
+
database_name = getattr(self, '_account_database_name', None)
|
|
1786
|
+
if database_name:
|
|
1787
|
+
base['database'] = dict(prefs['database'], **{'database.name': database_name})
|
|
1759
1788
|
path = accounts.write_private_json(
|
|
1760
1789
|
self._account_file(name),
|
|
1761
|
-
accounts.new_user_preferences(
|
|
1790
|
+
accounts.new_user_preferences(base, name, projects, database_password = password, s3_keys = keys))
|
|
1762
1791
|
print(f' ✓ wrote {path} (mode 600)')
|
|
1763
1792
|
print(f' Give it to {name} as <their labdata folder>/user_preferences.json, then delete your copy.')
|
|
1764
1793
|
if password is None:
|
|
@@ -1920,6 +1949,361 @@ examples:
|
|
|
1920
1949
|
write_log(final = True) # the last lines, often the summary
|
|
1921
1950
|
print('Pipe closed.')
|
|
1922
1951
|
|
|
1952
|
+
def _export_restriction(self, args):
|
|
1953
|
+
'''Return the restriction of -a/-s/-d as a list of dicts, or None.'''
|
|
1954
|
+
fields = [('subject_name', args.subject), ('session_name', args.session), ('dataset_name', args.datatype)]
|
|
1955
|
+
restriction = [dict()]
|
|
1956
|
+
for field, values in fields:
|
|
1957
|
+
if values:
|
|
1958
|
+
restriction = [dict(r, **{field: v}) for r in restriction for v in values]
|
|
1959
|
+
return None if restriction == [dict()] else restriction
|
|
1960
|
+
|
|
1961
|
+
def _export_common_arguments(self, parser):
|
|
1962
|
+
self._add_default_arguments(parser, level = 3)
|
|
1963
|
+
parser.add_argument('--plugin', nargs = '+', default = [],
|
|
1964
|
+
help = 'Plugins whose tables are seeds (restricted by -a/-s/-d where they have those fields).')
|
|
1965
|
+
parser.add_argument('--exclude', nargs = '+', default = None,
|
|
1966
|
+
help = 'Tables left out with their part tables (class names, e.g. ComputeTask Dataset.DataFiles).')
|
|
1967
|
+
parser.add_argument('--schema-map', nargs = '+', default = [],
|
|
1968
|
+
help = 'source=target names for databases that do not start with the lab database name.')
|
|
1969
|
+
parser.add_argument('-n', '--n-jobs', type = int, default = DEFAULT_N_JOBS,
|
|
1970
|
+
help = f'Copy threads (default {DEFAULT_N_JOBS}).')
|
|
1971
|
+
parser.add_argument('--dry-run', action = 'store_true', default = False,
|
|
1972
|
+
help = 'Print the tables, rows and estimated size; copy nothing.')
|
|
1973
|
+
parser.add_argument('--existing', default = 'ask', choices = ['ask', 'keep', 'update', 'replace'],
|
|
1974
|
+
help = 'Rows of the selection already in the target: ask (default), keep them, '
|
|
1975
|
+
'update their values, or replace them (delete the exported rows and copy them again).')
|
|
1976
|
+
parser.add_argument('--host', default = None, help = 'Another MySQL server to copy to: address or address:port.')
|
|
1977
|
+
parser.add_argument('--port', default = 3306, type = int, help = 'Port of --host when it has none (default 3306).')
|
|
1978
|
+
parser.add_argument('--db-user', default = None,
|
|
1979
|
+
help = 'Database user: the login for --host, or the account created in the --docker image (asked when not given).')
|
|
1980
|
+
parser.add_argument('--db-password', default = None,
|
|
1981
|
+
help = 'Its password (asked when not given; better asked than typed in the shell history).')
|
|
1982
|
+
|
|
1983
|
+
def _target_login(self, args):
|
|
1984
|
+
'''Return the --host target with its login from --db-user/--db-password, asked when not given.'''
|
|
1985
|
+
from getpass import getpass
|
|
1986
|
+
from .accounts import split_host
|
|
1987
|
+
print(f'Database login for {args.host} (used for this command only, not saved).')
|
|
1988
|
+
user = args.db_user or input(' User: ').strip()
|
|
1989
|
+
password = args.db_password if args.db_password is not None else getpass(f' Password for {user}: ')
|
|
1990
|
+
host, port = split_host(args.host, args.port)
|
|
1991
|
+
return dict(host = host, port = port, user = user, password = password)
|
|
1992
|
+
|
|
1993
|
+
def _lab_server_login(self):
|
|
1994
|
+
'''Ask for the login that writes the copy on the lab server, unless the environment has one.
|
|
1995
|
+
|
|
1996
|
+
The login is checked, then handed to this process in the environment (password encrypted)
|
|
1997
|
+
before the schema is loaded; it is not saved and stays out of the shell history.
|
|
1998
|
+
'''
|
|
1999
|
+
if os.environ.get('LABDATA_DATABASE_USER') and os.environ.get('LABDATA_DATABASE_PASS'):
|
|
2000
|
+
return
|
|
2001
|
+
import pymysql
|
|
2002
|
+
from getpass import getpass
|
|
2003
|
+
host = os.environ.get('LABDATA_DATABASE_HOST') or prefs['database'].get('database.host')
|
|
2004
|
+
default = prefs['database'].get('database.user') or ''
|
|
2005
|
+
print(f'Database login for {host} that writes the copy (used for this command only, not saved).')
|
|
2006
|
+
user = input(f' User [{default}]: ').strip() or default
|
|
2007
|
+
password = getpass(f' Password for {user}: ')
|
|
2008
|
+
from .accounts import split_host
|
|
2009
|
+
address, port = split_host(host)
|
|
2010
|
+
try:
|
|
2011
|
+
pymysql.connect(host = address, port = port, user = user, password = password).close()
|
|
2012
|
+
except pymysql.err.OperationalError as err:
|
|
2013
|
+
print(tcolor['r'](f'Could not log in as {user}: {err.args[-1]}'))
|
|
2014
|
+
exit(1)
|
|
2015
|
+
os.environ['LABDATA_DATABASE_USER'] = user
|
|
2016
|
+
os.environ['LABDATA_DATABASE_PASS'] = 'encrypted:' + encrypt_string(password)
|
|
2017
|
+
|
|
2018
|
+
def _image_login(self, args):
|
|
2019
|
+
'''Return the database account of a --docker image from --db-user/--db-password, asked when not given.'''
|
|
2020
|
+
from getpass import getpass
|
|
2021
|
+
user = args.db_user or input('Database account of the image [labdata]: ').strip() or 'labdata'
|
|
2022
|
+
password = args.db_password
|
|
2023
|
+
if password is None:
|
|
2024
|
+
password = getpass(f' Password for {user} [leave empty to generate one]: ')
|
|
2025
|
+
return user, password or None
|
|
2026
|
+
|
|
2027
|
+
def _print_export_access(self, report, target = None):
|
|
2028
|
+
'''Print who can read the databases of an export and how to give an account access.'''
|
|
2029
|
+
from . import accounts
|
|
2030
|
+
databases, project = report.attrs.get('databases', []), report.attrs.get('project')
|
|
2031
|
+
database_name = report.attrs.get('database_name')
|
|
2032
|
+
server = None
|
|
2033
|
+
if target:
|
|
2034
|
+
server = target['host'] if int(target.get('port', 3306)) == 3306 else f"{target['host']}:{target['port']}"
|
|
2035
|
+
where = f'on {server}' if target else 'on this server'
|
|
2036
|
+
try:
|
|
2037
|
+
if target:
|
|
2038
|
+
from .export.selection import _open_connection
|
|
2039
|
+
conn = _open_connection(target)
|
|
2040
|
+
else:
|
|
2041
|
+
import datajoint as dj
|
|
2042
|
+
conn = dj.conn()
|
|
2043
|
+
access = accounts.database_access(conn, databases)
|
|
2044
|
+
except Exception as err:
|
|
2045
|
+
print(f'Could not read the database grants {where}: {err}')
|
|
2046
|
+
access = None
|
|
2047
|
+
if access is not None:
|
|
2048
|
+
everything = [u for u, names in access.items() if '*' in names]
|
|
2049
|
+
some = {u: names for u, names in access.items() if '*' not in names}
|
|
2050
|
+
print(f'\nAccess to {", ".join(databases)} {where} (database grants):')
|
|
2051
|
+
for user, names in some.items():
|
|
2052
|
+
print(f' {user}: {", ".join(names)}')
|
|
2053
|
+
if not some:
|
|
2054
|
+
print(' no account has grants on these databases.')
|
|
2055
|
+
if everything:
|
|
2056
|
+
print(f' and the accounts with grants on every database: {", ".join(everything)}')
|
|
2057
|
+
option = '' if database_name == prefs['database'].get('database.name') else f' --database-name {database_name}'
|
|
2058
|
+
print('\nTo give an account access to the project (database grants, Project.User and, '
|
|
2059
|
+
'with an IAM user, the S3 group of the project):')
|
|
2060
|
+
if target:
|
|
2061
|
+
print(f' LABDATA_PREFERENCES=<preference file for {server}> '
|
|
2062
|
+
f'labdata account grant <name> -p {project}{option}')
|
|
2063
|
+
print(f' (labdata account uses the server of the preference file; '
|
|
2064
|
+
f'"labdata account create <name> -p {project}{option}" for a new account)')
|
|
2065
|
+
else:
|
|
2066
|
+
print(f' labdata account grant <name> -p {project}{option}')
|
|
2067
|
+
print(f' labdata account create <name> -p {project}{option} # a new account')
|
|
2068
|
+
|
|
2069
|
+
def _print_export_report(self, report):
|
|
2070
|
+
copied = report[report['rows'].fillna(0) > 0]
|
|
2071
|
+
mode = report.attrs.get('existing', 'keep')
|
|
2072
|
+
for row in copied.itertuples():
|
|
2073
|
+
existing = int(row.existing or 0) if 'existing' in report else 0
|
|
2074
|
+
note = f', {existing} were there ({mode})' if existing else ''
|
|
2075
|
+
print(f'\t{row.table} {int(row.rows)} rows{note}')
|
|
2076
|
+
print(tcolor['g'](f"Done: {int(copied['rows'].sum())} rows in {len(copied)} tables."))
|
|
2077
|
+
|
|
2078
|
+
def share(self):
|
|
2079
|
+
'''Copy tables from one project to another, with their parents and part tables.
|
|
2080
|
+
|
|
2081
|
+
The lab-wide tables (File, LabMember, Setup, ...) are shared by the projects and are not
|
|
2082
|
+
copied. The copy runs on the database server. Needs read access to the source project and
|
|
2083
|
+
write access to the target databases.
|
|
2084
|
+
|
|
2085
|
+
Usage
|
|
2086
|
+
-----
|
|
2087
|
+
::
|
|
2088
|
+
|
|
2089
|
+
labdata share EphysRecording --from couto_arc --to other_project -a JC1013
|
|
2090
|
+
labdata share EphysRecording --from couto_arc --to other_project --descendants --dry-run
|
|
2091
|
+
labdata share --plugin ratetask --from couto_arc --to other_project
|
|
2092
|
+
labdata share EphysRecording --from couto_arc --host 10.0.0.5 --db-user admin -a JC1013
|
|
2093
|
+
labdata share Dataset.DataFiles --from couto_arc --to other_project --database-name newlab -a JC1013
|
|
2094
|
+
|
|
2095
|
+
With --host the project goes to another MySQL server (same project name unless --to), with
|
|
2096
|
+
the lab-wide rows it uses (File, LabMember, ...); the password is asked. With a new
|
|
2097
|
+
--database-name on this server it goes to the databases ``newlab`` and
|
|
2098
|
+
``newlab_<project>...``, with the lab-wide rows it uses. The preference entries that open
|
|
2099
|
+
the copy are written to a JSON file (--access-file).
|
|
2100
|
+
'''
|
|
2101
|
+
parser = argparse.ArgumentParser(
|
|
2102
|
+
description = 'Share tables, with their parents and part tables, with another project.',
|
|
2103
|
+
usage = 'labdata share <table ...> --from <project> --to <project> [-a subject] [-s session] [options]',
|
|
2104
|
+
formatter_class = argparse.RawDescriptionHelpFormatter,
|
|
2105
|
+
epilog = '''examples:
|
|
2106
|
+
labdata share EphysRecording --from couto_arc --to other_project -a JC1013 --dry-run
|
|
2107
|
+
labdata share EphysRecording --from couto_arc --to other_project --descendants # with spike sorting
|
|
2108
|
+
labdata share --plugin ratetask --from couto_arc --to other_project -a JC1013
|
|
2109
|
+
labdata share EphysRecording --from couto_arc --host 10.0.0.5 --db-user admin -a JC1013
|
|
2110
|
+
|
|
2111
|
+
# the File rows of the datasets of JC1013 (with their Session and Dataset rows), on this
|
|
2112
|
+
# server, to the lab-wide database newlab and the project databases newlab_other_project...
|
|
2113
|
+
labdata share Dataset.DataFiles --from couto_arc --to other_project --database-name newlab -a JC1013
|
|
2114
|
+
|
|
2115
|
+
The lab-wide database (File, LabMember, Setup, ...) is shared by the projects of a server and is
|
|
2116
|
+
not copied, unless --host or a new --database-name is given; then only the rows the selection
|
|
2117
|
+
uses are copied. Use a table with the -a/-s/-d fields as seed (File has none, so -a/-s/-d would
|
|
2118
|
+
not restrict it).
|
|
2119
|
+
|
|
2120
|
+
After the copy, the entries that open the target are written to a JSON file (--access-file,
|
|
2121
|
+
default labdata_access_<database name>_<project>.json): copy them into the "database" section of
|
|
2122
|
+
the preference file, or set them with LABDATA_DATABASE_PROJECT for the project.
|
|
2123
|
+
''')
|
|
2124
|
+
parser.add_argument('table', nargs = '*', default = [],
|
|
2125
|
+
help = 'Seed tables (class names, e.g. EphysRecording, SpikeSorting).')
|
|
2126
|
+
parser.add_argument('--from', dest = 'source', required = True, help = 'Source project.')
|
|
2127
|
+
parser.add_argument('--to', dest = 'target', default = None,
|
|
2128
|
+
help = 'Target project (default with --host: the source project).')
|
|
2129
|
+
parser.add_argument('--database-name', default = None,
|
|
2130
|
+
help = 'database.name of the lab in the target (default: the same as here). On this server, '
|
|
2131
|
+
'a new name copies the lab-wide rows the selection uses to that database.')
|
|
2132
|
+
parser.add_argument('--access-file', default = None,
|
|
2133
|
+
help = 'JSON file with the preference entries that open the target '
|
|
2134
|
+
'(default: labdata_access_<database name>_<project>.json).')
|
|
2135
|
+
parser.add_argument('--descendants', action = 'store_true', default = False,
|
|
2136
|
+
help = 'Also copy the tables derived from the seeds (spike sorting, ...).')
|
|
2137
|
+
self._export_common_arguments(parser)
|
|
2138
|
+
args = parser.parse_args(sys.argv[2:])
|
|
2139
|
+
if not args.table and not args.plugin:
|
|
2140
|
+
parser.error('give a table or --plugin.')
|
|
2141
|
+
if args.target is None and args.host is None and args.database_name is None:
|
|
2142
|
+
parser.error('give --to, --host or --database-name.')
|
|
2143
|
+
target = None
|
|
2144
|
+
if args.host is not None and not args.dry_run:
|
|
2145
|
+
target = self._target_login(args)
|
|
2146
|
+
elif args.host is not None:
|
|
2147
|
+
from .accounts import split_host
|
|
2148
|
+
address, port = split_host(args.host, args.port)
|
|
2149
|
+
target = dict(host = address, port = port)
|
|
2150
|
+
elif not args.dry_run:
|
|
2151
|
+
self._lab_server_login()
|
|
2152
|
+
from .export import share_project, DEFAULT_EXCLUDE, ExportCancelled
|
|
2153
|
+
try:
|
|
2154
|
+
report = share_project(args.table or None, args.source, args.target,
|
|
2155
|
+
target = target, database_name = args.database_name, existing = args.existing,
|
|
2156
|
+
restriction = self._export_restriction(args), plugins = args.plugin,
|
|
2157
|
+
descendants = args.descendants,
|
|
2158
|
+
exclude = DEFAULT_EXCLUDE if args.exclude is None else args.exclude,
|
|
2159
|
+
schema_map = dict(m.split('=', 1) for m in args.schema_map),
|
|
2160
|
+
n_jobs = args.n_jobs, dry_run = args.dry_run)
|
|
2161
|
+
except ExportCancelled as err:
|
|
2162
|
+
print(err)
|
|
2163
|
+
return
|
|
2164
|
+
except ValueError as err:
|
|
2165
|
+
print(tcolor['r'](str(err)))
|
|
2166
|
+
exit(1)
|
|
2167
|
+
if not args.dry_run:
|
|
2168
|
+
self._print_export_report(report)
|
|
2169
|
+
from .export import project_access
|
|
2170
|
+
host = None
|
|
2171
|
+
if target is not None:
|
|
2172
|
+
host = target['host'] if target['port'] == 3306 else f"{target['host']}:{target['port']}"
|
|
2173
|
+
access = project_access(args.target or args.source, args.database_name, host)
|
|
2174
|
+
name = access['database']['database.name']
|
|
2175
|
+
path = Path(args.access_file or f'labdata_access_{name}_{access["database"]["database.project"]}.json')
|
|
2176
|
+
with open(path, 'w') as fd:
|
|
2177
|
+
json.dump(access, fd, indent = 4)
|
|
2178
|
+
print(f'Preference entries to open {name} / {access["database"]["database.project"]} in {path}; '
|
|
2179
|
+
'copy them into the "database" section of the preference file.')
|
|
2180
|
+
self._print_export_access(report, target)
|
|
2181
|
+
|
|
2182
|
+
def export(self):
|
|
2183
|
+
'''Copy a selection, with the lab-wide rows it uses, to its own database, a dump or a Docker image.
|
|
2184
|
+
|
|
2185
|
+
Derived tables are included unless --no-descendants. Raw files stay where they are unless
|
|
2186
|
+
--files bucket (copied to another storage) or --files download (to a folder).
|
|
2187
|
+
|
|
2188
|
+
Usage
|
|
2189
|
+
-----
|
|
2190
|
+
::
|
|
2191
|
+
|
|
2192
|
+
labdata export EphysRecording -p couto_arc -a JC1013 --dry-run
|
|
2193
|
+
labdata export EphysRecording -p couto_arc -a JC1013 --database-name release_2026
|
|
2194
|
+
labdata export EphysRecording -p couto_arc --host 10.0.0.5 --db-user admin
|
|
2195
|
+
labdata export EphysRecording -p couto_arc --dump release_dump
|
|
2196
|
+
labdata export EphysRecording -p couto_arc --docker release --tag lab/release:1 \\
|
|
2197
|
+
--pip git+https://github.com/org/package --blank-personal --build
|
|
2198
|
+
labdata export EphysRecording -p couto_arc --docker release --link-data --dump release_data \\
|
|
2199
|
+
--tag lab/release:1 --build
|
|
2200
|
+
|
|
2201
|
+
Run the image with ``docker run -p 8888:8888 <tag>`` (Jupyter) or
|
|
2202
|
+
``docker run -p 8501:8501 <tag> dashboard`` (dashboard). With --link-data the rows are not
|
|
2203
|
+
in the image: mount the data folder with ``-v <data folder>:/dump:ro`` and keep the loaded
|
|
2204
|
+
database in a volume with ``-v <volume>:/var/lib/mysql``.
|
|
2205
|
+
'''
|
|
2206
|
+
parser = argparse.ArgumentParser(
|
|
2207
|
+
description = 'Export a selection to a database, a dump folder or a Docker image.',
|
|
2208
|
+
usage = 'labdata export <table ...> [-p project] [-a subject] [--database-name name] '
|
|
2209
|
+
'[--host h --user u | --dump folder | --docker folder --tag tag [--link-data]] [options]')
|
|
2210
|
+
parser.add_argument('table', nargs = '*', default = [],
|
|
2211
|
+
help = 'Seed tables (class names, e.g. EphysRecording).')
|
|
2212
|
+
project = _get_project()
|
|
2213
|
+
parser.add_argument('-p', '--project', default = project, type = str, help = f'Source project: {project}')
|
|
2214
|
+
parser.add_argument('--database-name', default = None,
|
|
2215
|
+
help = 'New database.name of the exported copy (required on the lab server).')
|
|
2216
|
+
parser.add_argument('--dump', default = None,
|
|
2217
|
+
help = 'Write gzipped SQL files to this folder (with --docker --link-data: the data folder).')
|
|
2218
|
+
parser.add_argument('--docker', default = None, help = 'Write a Docker build context to this folder.')
|
|
2219
|
+
parser.add_argument('--link-data', action = 'store_true', default = False,
|
|
2220
|
+
help = 'With --docker: leave the rows out of the image; they go to a separate data folder '
|
|
2221
|
+
'(--dump, default <docker folder>_data) mounted at /dump when the container starts.')
|
|
2222
|
+
parser.add_argument('--tag', default = None, help = 'Image tag (with --docker).')
|
|
2223
|
+
parser.add_argument('--build', action = 'store_true', default = False, help = 'Run docker build (with --docker).')
|
|
2224
|
+
parser.add_argument('--platform', default = None, help = 'docker build --platform, e.g. linux/amd64.')
|
|
2225
|
+
parser.add_argument('--pip', nargs = '+', default = [],
|
|
2226
|
+
help = 'Packages to install in the image (PyPI names or git+https:// URLs).')
|
|
2227
|
+
parser.add_argument('--requirements', default = None, help = 'Requirements file to install in the image.')
|
|
2228
|
+
parser.add_argument('--labdata-source', default = None,
|
|
2229
|
+
help = 'pip source of labdata for the image (default: the code running now).')
|
|
2230
|
+
parser.add_argument('--no-dashboard', action = 'store_true', default = False,
|
|
2231
|
+
help = 'Leave the dashboard dependencies out of the image.')
|
|
2232
|
+
parser.add_argument('--pref', nargs = '+', default = [],
|
|
2233
|
+
help = 'key=value entries for the preference file of the image (value read as JSON when it parses).')
|
|
2234
|
+
parser.add_argument('--overwrite', action = 'store_true', default = False,
|
|
2235
|
+
help = 'Replace an existing --docker folder (and its data folder with --link-data).')
|
|
2236
|
+
parser.add_argument('--files', default = 'none', choices = ['none', 'bucket', 'download'],
|
|
2237
|
+
help = 'Raw files: leave them (none), copy them to --files-storage (bucket) or to --files-path (download).')
|
|
2238
|
+
parser.add_argument('--files-storage', default = None, help = 'Storage name of the preference file for --files bucket.')
|
|
2239
|
+
parser.add_argument('--files-path', default = None, help = 'Folder for --files download.')
|
|
2240
|
+
parser.add_argument('--blank-personal', action = 'store_true', default = False,
|
|
2241
|
+
help = 'Export LabMember rows with email, first and last name set to NULL.')
|
|
2242
|
+
parser.add_argument('--strip', nargs = '+', default = [],
|
|
2243
|
+
help = 'Further nullable columns to export as NULL: Table.column (e.g. Subject.subject_dob).')
|
|
2244
|
+
parser.add_argument('--no-descendants', action = 'store_true', default = False,
|
|
2245
|
+
help = 'Leave out the tables derived from the seeds.')
|
|
2246
|
+
self._export_common_arguments(parser)
|
|
2247
|
+
args = parser.parse_args(sys.argv[2:])
|
|
2248
|
+
if not args.table and not args.plugin:
|
|
2249
|
+
parser.error('give a table or --plugin.')
|
|
2250
|
+
if args.link_data and args.docker is None:
|
|
2251
|
+
parser.error('--link-data is used with --docker.')
|
|
2252
|
+
if args.host is not None and (args.dump is not None or args.docker is not None):
|
|
2253
|
+
parser.error('use one of --host, --dump or --docker.')
|
|
2254
|
+
if args.dump is not None and args.docker is not None and not args.link_data:
|
|
2255
|
+
parser.error('use one of --dump or --docker, or both with --link-data (the dump is the data folder).')
|
|
2256
|
+
from .export import export, EXPORT_EXCLUDE, ExportCancelled
|
|
2257
|
+
strip = dict()
|
|
2258
|
+
for item in args.strip:
|
|
2259
|
+
table, column = item.rsplit('.', 1)
|
|
2260
|
+
strip.setdefault(table, []).append(column)
|
|
2261
|
+
prefs_extra = dict()
|
|
2262
|
+
for item in args.pref:
|
|
2263
|
+
key, value = item.split('=', 1)
|
|
2264
|
+
try:
|
|
2265
|
+
prefs_extra[key] = json.loads(value)
|
|
2266
|
+
except ValueError:
|
|
2267
|
+
prefs_extra[key] = value
|
|
2268
|
+
options = dict(project = args.project, restriction = self._export_restriction(args), plugins = args.plugin,
|
|
2269
|
+
database_name = args.database_name, descendants = not args.no_descendants,
|
|
2270
|
+
exclude = EXPORT_EXCLUDE if args.exclude is None else args.exclude,
|
|
2271
|
+
schema_map = dict(m.split('=', 1) for m in args.schema_map),
|
|
2272
|
+
blank_personal = args.blank_personal, strip_columns = strip,
|
|
2273
|
+
files = args.files, files_storage = args.files_storage, files_path = args.files_path,
|
|
2274
|
+
existing = args.existing, n_jobs = args.n_jobs, dry_run = args.dry_run)
|
|
2275
|
+
if args.docker is not None:
|
|
2276
|
+
from .export import build_export_image
|
|
2277
|
+
db_user, db_password = ('labdata', None) if args.dry_run else self._image_login(args)
|
|
2278
|
+
options.pop('existing') # an image is built from a new dump folder
|
|
2279
|
+
result = build_export_image(args.table or None, args.docker, tag = args.tag, pip = args.pip,
|
|
2280
|
+
db_user = db_user, db_password = db_password,
|
|
2281
|
+
requirements = args.requirements, labdata_source = args.labdata_source,
|
|
2282
|
+
dashboard = not args.no_dashboard, platform = args.platform,
|
|
2283
|
+
build = args.build, prefs_extra = prefs_extra,
|
|
2284
|
+
link_data = args.link_data, data_dir = args.dump,
|
|
2285
|
+
overwrite = args.overwrite, **options)
|
|
2286
|
+
else:
|
|
2287
|
+
target = None
|
|
2288
|
+
if args.host is not None and not args.dry_run:
|
|
2289
|
+
target = self._target_login(args)
|
|
2290
|
+
elif args.dump is not None:
|
|
2291
|
+
target = args.dump
|
|
2292
|
+
elif not args.dry_run:
|
|
2293
|
+
self._lab_server_login()
|
|
2294
|
+
try:
|
|
2295
|
+
result = export(args.table or None, target, **options)
|
|
2296
|
+
except ExportCancelled as err:
|
|
2297
|
+
print(err)
|
|
2298
|
+
return
|
|
2299
|
+
except ValueError as err:
|
|
2300
|
+
print(tcolor['r'](str(err)))
|
|
2301
|
+
exit(1)
|
|
2302
|
+
if not args.dry_run:
|
|
2303
|
+
self._print_export_report(result['report'])
|
|
2304
|
+
if args.docker is not None and not args.build:
|
|
2305
|
+
print(f"Build the image with: docker build -t {args.tag or '<tag>'} {args.docker}")
|
|
2306
|
+
|
|
1923
2307
|
def run_container(self):
|
|
1924
2308
|
'''
|
|
1925
2309
|
Runs a container
|
|
@@ -2051,7 +2435,54 @@ examples:
|
|
|
2051
2435
|
type = str,
|
|
2052
2436
|
help= f'Select project: {project}')
|
|
2053
2437
|
return parser
|
|
2054
|
-
|
|
2438
|
+
|
|
2439
|
+
def _print_compute_tasks(self, schema, analysis = None, limit = 10):
|
|
2440
|
+
'''Print the ComputeTask table by status, with the last line of each task log.
|
|
2441
|
+
|
|
2442
|
+
Shows the most recent ``limit`` tasks of each status and the count of the rest.
|
|
2443
|
+
'''
|
|
2444
|
+
tasks = schema.ComputeTask()
|
|
2445
|
+
if not analysis is None:
|
|
2446
|
+
tasks = tasks & dict(task_name = analysis)
|
|
2447
|
+
title = 'ComputeTask table' + (f' [{analysis}]' if not analysis is None else '')
|
|
2448
|
+
print(tcolor['m'](f"++++++++++++++++++++ {title} ++++++++++++++++++++"))
|
|
2449
|
+
if not len(tasks):
|
|
2450
|
+
print('\t no compute tasks.')
|
|
2451
|
+
return
|
|
2452
|
+
statuses = [('WORKING', tcolor['c']), ('WAITING', tcolor['y']), ('ON SERVER', tcolor['b']),
|
|
2453
|
+
('FAILED', tcolor['r']), ('CANCELED', tcolor['o']), ('COMPLETED', tcolor['g'])]
|
|
2454
|
+
def _brief(log, width = 100):
|
|
2455
|
+
if log is None:
|
|
2456
|
+
return ''
|
|
2457
|
+
if isinstance(log, bytes):
|
|
2458
|
+
log = log.decode('utf-8', errors = 'replace')
|
|
2459
|
+
lines = [l.strip() for l in str(log).splitlines() if len(l.strip())]
|
|
2460
|
+
line = ' '.join(lines[-1].split()) if len(lines) else ''
|
|
2461
|
+
return line if len(line) <= width else line[:width-3] + '...'
|
|
2462
|
+
def _time(t):
|
|
2463
|
+
return t.strftime('%Y-%m-%d %H:%M') if not t is None else ''
|
|
2464
|
+
for status, cc in statuses:
|
|
2465
|
+
sel = tasks & f'task_status LIKE "%{status}%"'
|
|
2466
|
+
n = len(sel)
|
|
2467
|
+
if not n:
|
|
2468
|
+
continue
|
|
2469
|
+
print(cc(f"{status} ({n})"))
|
|
2470
|
+
rows = sel.fetch('job_id', 'task_name', 'task_target', 'task_host', 'subject_name',
|
|
2471
|
+
'session_name', 'task_starttime', 'task_endtime', 'task_log',
|
|
2472
|
+
as_dict = True, order_by = 'job_id DESC', limit = limit)
|
|
2473
|
+
for r in rows:
|
|
2474
|
+
where = '/'.join([str(r[k]) for k in ('subject_name', 'session_name') if not r[k] is None])
|
|
2475
|
+
when = _time(r['task_endtime'] if status in ('COMPLETED', 'FAILED', 'CANCELED') and
|
|
2476
|
+
not r['task_endtime'] is None else r['task_starttime'])
|
|
2477
|
+
print(f"\t {r['job_id']} {r['task_name']} {where} "
|
|
2478
|
+
f"target: {r['task_target']} host: {r['task_host']}" +
|
|
2479
|
+
(f" {tcolor['y'](when)}" if len(when) else ''))
|
|
2480
|
+
log = _brief(r['task_log'])
|
|
2481
|
+
if len(log):
|
|
2482
|
+
print(f"\t\t{log}")
|
|
2483
|
+
if n > limit:
|
|
2484
|
+
print(f"\t ... and {n - limit} more")
|
|
2485
|
+
|
|
2055
2486
|
def _get_default_arg(self,argument,cli_arg = 'submit', default = None):
|
|
2056
2487
|
# checks if there is a default in the options
|
|
2057
2488
|
if not f'{cli_arg}_defaults' in labdata_preferences.keys():
|
|
@@ -5,6 +5,7 @@ Tasks can be scheduled and run on dedicated containers using job schedulers.
|
|
|
5
5
|
|
|
6
6
|
Available compute tasks:
|
|
7
7
|
- SpksCompute: Spike sorting using Kilosort/Phy via SPKS
|
|
8
|
+
- SpikeInterfaceCompute: Spike sorting with the sorters of SpikeInterface
|
|
8
9
|
- DeeplabcutCompute: Animal pose estimation using DeepLabCut
|
|
9
10
|
- LightningPoseCompute: Animal pose estimation using Lightning Pose
|
|
10
11
|
|
|
@@ -32,6 +33,7 @@ The compute tasks handle:
|
|
|
32
33
|
from .utils import *
|
|
33
34
|
from .singularity import build_singularity_container
|
|
34
35
|
from .ephys import SpksCompute
|
|
36
|
+
from .spikeinterface import SpikeInterfaceCompute
|
|
35
37
|
from .pose import DeeplabcutCompute, LightningPoseCompute
|
|
36
38
|
from .caiman import CaimanCompute
|
|
37
39
|
from .suite2p import Suite2pCompute
|