batch2p 0.1.0__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.
batch2p/multi.py ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """batch2p-multi — generate one data.json per directory from a template.
3
+
4
+ Usage:
5
+ batch2p-multi <template_data.json> <dir_list.txt> --params-file <params.json> [--output-dir <dir>]
6
+
7
+ The template data.json may contain {{ job_id }} and {{ root_dir }} placeholders,
8
+ which are substituted per-directory at generation time. The dir_list file must
9
+ contain one directory path per line (blank lines and lines starting with # are
10
+ ignored).
11
+
12
+ For each directory the script:
13
+ - collects all .tif/.tiff files and .b64 files (lexicographically sorted),
14
+ - substitutes {{ job_id }} and {{ root_dir }} in all string fields,
15
+ - writes <output_dir>/<job_id>_<n>_data.json (1-based index, zero-padded).
16
+ """
17
+ import argparse
18
+ import json
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ def _apply_template_vars(obj, variables: dict):
25
+ """Recursively replace {{ var }} placeholders in string values.
26
+
27
+ Placeholders whose variable is absent or empty are left verbatim.
28
+ """
29
+ if isinstance(obj, str):
30
+ def _replace(m):
31
+ val = variables.get(m.group(1).strip())
32
+ return val if val else m.group(0)
33
+ return re.sub(r'\{\{\s*(\w+)\s*\}\}', _replace, obj)
34
+ if isinstance(obj, dict):
35
+ return {k: _apply_template_vars(v, variables) for k, v in obj.items()}
36
+ if isinstance(obj, list):
37
+ return [_apply_template_vars(x, variables) for x in obj]
38
+ return obj
39
+
40
+
41
+ def _replace_literal(obj, old: str, new: str):
42
+ """Recursively replace all occurrences of `old` with `new` in string values."""
43
+ if isinstance(obj, str):
44
+ return obj.replace(old, new)
45
+ if isinstance(obj, dict):
46
+ return {k: _replace_literal(v, old, new) for k, v in obj.items()}
47
+ if isinstance(obj, list):
48
+ return [_replace_literal(x, old, new) for x in obj]
49
+ return obj
50
+
51
+
52
+ def _collect_files(directory: Path, pattern: str) -> list[str]:
53
+ return sorted(str(p) for p in directory.glob(pattern))
54
+
55
+
56
+ def main():
57
+ parser = argparse.ArgumentParser(
58
+ description="Generate one data.json per directory from a template."
59
+ )
60
+ parser.add_argument("template", help="Template data.json with {{ }} placeholders.")
61
+ parser.add_argument("dir_list", help="Text file with one directory per line.")
62
+ parser.add_argument("--params-file", required=True,
63
+ help="Path to the params.json file (same for all outputs).")
64
+ parser.add_argument("--output-dir", default=".",
65
+ help="Directory where generated data.json files are written (default: cwd).")
66
+ parser.add_argument("--tif-regexp", default=None,
67
+ help="Only include .tif/.tiff files whose filename matches this regexp.")
68
+ parser.add_argument("--force-sync", action="store_true",
69
+ help="Require .b64 files to be present and match the number of tif files; "
70
+ "skip directories where this condition is not met.")
71
+ args = parser.parse_args()
72
+
73
+ template_path = Path(args.template)
74
+ dir_list_path = Path(args.dir_list)
75
+ params_file = str(Path(args.params_file).resolve())
76
+ out_dir = Path(args.output_dir)
77
+
78
+ tif_regexp = re.compile(args.tif_regexp) if args.tif_regexp else None
79
+
80
+ # Load template
81
+ if not template_path.exists():
82
+ sys.exit(f"Template not found: {template_path}")
83
+ with open(template_path) as f:
84
+ template = json.load(f)
85
+
86
+ # Load directory list
87
+ if not dir_list_path.exists():
88
+ sys.exit(f"Directory list not found: {dir_list_path}")
89
+ directories = []
90
+ with open(dir_list_path) as f:
91
+ for raw in f:
92
+ line = raw.strip()
93
+ if not line or line.startswith("#"):
94
+ continue
95
+ p = Path(line)
96
+ if not p.is_dir():
97
+ print(f"Warning: not a directory, skipping: {line}", file=sys.stderr)
98
+ continue
99
+ directories.append(p)
100
+
101
+ if not directories:
102
+ sys.exit("No valid directories found in the list.")
103
+
104
+ out_dir.mkdir(parents=True, exist_ok=True)
105
+
106
+ # Derive base job_id from the template (may itself contain a placeholder)
107
+ base_job_id = template.get("job_id", "run")
108
+ digits = len(str(len(directories)))
109
+
110
+ # If the template has a literal root_path, treat it as the prototype root: every
111
+ # occurrence of that path in any string field will be replaced with the current
112
+ # directory. This lets real data.json files (with no {{ }} placeholders) be used
113
+ # directly as templates without manual editing.
114
+ proto_root = template.get("root_path", "").rstrip("/")
115
+
116
+ generated = []
117
+ for idx, directory in enumerate(directories, start=1):
118
+ root_dir = str(directory.absolute())
119
+
120
+ # Collect files
121
+ tif_files = sorted(set(
122
+ _collect_files(directory, "*.tif") +
123
+ _collect_files(directory, "*.tiff")
124
+ ))
125
+ if tif_regexp is not None:
126
+ tif_files = [p for p in tif_files if tif_regexp.search(Path(p).name)]
127
+ if not tif_files:
128
+ print(f"Warning: no tif files found, skipping: {directory}", file=sys.stderr)
129
+ continue
130
+ b64_files = _collect_files(directory, "*.b64")
131
+ if args.force_sync:
132
+ if not b64_files:
133
+ print(f"Warning: --force-sync: no b64 files found, skipping: {directory}", file=sys.stderr)
134
+ continue
135
+ if len(b64_files) != len(tif_files):
136
+ print(f"Warning: --force-sync: {len(b64_files)} b64 vs {len(tif_files)} tif, skipping: {directory}", file=sys.stderr)
137
+ continue
138
+
139
+ # Substitute variables in the template (produces the per-dir job_id).
140
+ # Step 1: replace the prototype root_path literally in all string fields so
141
+ # that templates with real paths (no {{ }} placeholders) work correctly.
142
+ # Step 2: apply {{ }} placeholder substitution for explicit placeholders.
143
+ suffix = f"_{idx:0{digits}d}"
144
+ working = _replace_literal(template, proto_root, root_dir) if proto_root else template
145
+ resolved_base_job_id = _apply_template_vars(
146
+ _replace_literal(base_job_id, proto_root, root_dir) if proto_root else base_job_id,
147
+ {"root_dir": root_dir},
148
+ )
149
+ variables = {"job_id": resolved_base_job_id, "root_dir": root_dir}
150
+ data_dict = _apply_template_vars(working, variables)
151
+
152
+ # Resolve the job_id after substitution, then append numeric suffix
153
+ resolved_job_id = data_dict.get("job_id", resolved_base_job_id)
154
+ final_job_id = f"{resolved_job_id}{suffix}"
155
+
156
+ data_dict["job_id"] = final_job_id
157
+ data_dict["params_file"] = params_file
158
+ data_dict["root_path"] = root_dir
159
+ data_dict["data"] = tif_files
160
+ if b64_files:
161
+ data_dict["behavior_data"] = b64_files
162
+ elif "behavior_data" in data_dict:
163
+ del data_dict["behavior_data"]
164
+
165
+ out_path = out_dir / f"{final_job_id}_data.json"
166
+ with open(out_path, "w") as f:
167
+ json.dump(data_dict, f, indent=2)
168
+
169
+ generated.append(out_path)
170
+ print(f" [{idx:>{digits}}/{len(directories)}] {out_path.name} ({len(tif_files)} tif, {len(b64_files)} b64) {root_dir}")
171
+
172
+ print(f"\nGenerated {len(generated)} data.json file(s) in: {out_dir.resolve()}")
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: batch2p
3
+ Version: 0.1.0
4
+ Author: Francesco P. Battaglia
5
+ License-Expression: GPL-3.0-or-later
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: numpy
10
+ Requires-Dist: pynapple
11
+ Requires-Dist: suite2p
12
+ Requires-Dist: tifffile
13
+ Requires-Dist: tifftrim
14
+ Requires-Dist: totalsync-2p
15
+ Requires-Dist: totalsync-utils
16
+ Provides-Extra: gui
17
+ Requires-Dist: pyqt5; extra == 'gui'
@@ -0,0 +1,13 @@
1
+ batch2p/__init__.py,sha256=L3g3zkH1gR1Igqp0olvgeWwpSxEXqC4qbTx8ldPQATg,56
2
+ batch2p/cli.py,sha256=PsHrTxl8fjyFmWapu4bMkH527jpVCxDLnmAV4uzFFjA,17731
3
+ batch2p/compute_F_sub.py,sha256=QEPU4rPtEwLBVyET-urM3Pqc1uEHiZcPWjq3wWaAcVE,5249
4
+ batch2p/gui.py,sha256=WRgFrDio47fHDrxj3nzBqPE_XGC3HWto_iHs2k-cH70,74149
5
+ batch2p/multi.py,sha256=KPfcDELcroNEJZ-b7cI8w6648Jr-_YmgdQ4H1gRpnTg,7387
6
+ batch2p/extractors/__init__.py,sha256=bYtbjPhfst7z6sodVsJ2T3XRPrsWfOmNVUyM216odZs,470
7
+ batch2p/extractors/base.py,sha256=l23PUW71DHv2k7vFc3rShE13lOuFxXtKN9e2Fal602c,1089
8
+ batch2p/extractors/suite2p.py,sha256=eVYmkicuTmSYLHFIgNpkmlAfG3R90TSdDTMOJ7KWE1c,13267
9
+ batch2p/extractors/suite3d.py,sha256=wsuRteMgYH3Yi-blEHtnTe8ny4oV7l1fKBINFYSKykc,5609
10
+ batch2p-0.1.0.dist-info/METADATA,sha256=FtmtoM9qBN3A6jhOAs56KYhHIMwf2mc9NmAvRY6VmWk,470
11
+ batch2p-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
+ batch2p-0.1.0.dist-info/entry_points.txt,sha256=lFNxgFg89kcBWBqakObR0uPdz80N3Z5VnJMcdpkEL8Y,161
13
+ batch2p-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ batch2p = batch2p.cli:main
3
+ batch2p-compute-fsub = batch2p.compute_F_sub:main
4
+ batch2p-gui = batch2p.gui:main
5
+ batch2p-multi = batch2p.multi:main