|
1 | 1 | """Per-visit settings for the XChem autoprocessing pipelines. |
2 | 2 |
|
3 | | -A labxchem visit may hold a `.user.yaml` naming its target and steering how |
4 | | -it is processed:: |
| 3 | +Settings live in a `.user.yaml` the users own, under a visit's `processing` |
| 4 | +directory, which the pipeline only ever reads:: |
5 | 5 |
|
6 | | - data: |
7 | | - acronym: A71EV2A # cached by the pipeline |
| 6 | + # <visit>/processing/.user.yaml |
8 | 7 | autoprocessing: |
9 | 8 | enabled: true # process this visit at all? |
10 | 9 | comparator_threshold: 150 # datasets PanDDA2 waits for before starting |
|
13 | 12 | high_res_lower_limit: 2.5 |
14 | 13 | notify: someone@diamond.ac.uk # who to mail when collate finishes |
15 | 14 |
|
| 15 | +The target acronym is cached separately, in a `.user.yaml` at the top of the |
| 16 | +visit, because that is the one the pipeline can write:: |
| 17 | +
|
| 18 | + # <visit>/.user.yaml |
| 19 | + data: |
| 20 | + acronym: A71EV2A |
| 21 | +
|
| 22 | +Visits predating the split keep their settings alongside the cached acronym, |
| 23 | +and are still read from there when they have no `processing` file. |
| 24 | +
|
16 | 25 | A recipe that set the parameter explicitly beats the file, the file |
17 | 26 | beats the parameter model's default. |
18 | 27 | """ |
|
25 | 34 | import pydantic |
26 | 35 | import yaml |
27 | 36 |
|
28 | | -CONFIG_FILENAME = ".user.yaml" |
| 37 | +# written by the pipeline, holding only the cached acronym |
| 38 | +CACHE_FILE = ".user.yaml" |
| 39 | +# written by users; the pipeline never writes here |
| 40 | +SETTINGS_FILE = "processing/.user.yaml" |
29 | 41 |
|
30 | 42 |
|
31 | 43 | class VisitConfig(pydantic.BaseModel): |
@@ -62,43 +74,56 @@ def _mapping(value) -> dict: |
62 | 74 | return value if isinstance(value, dict) else {} |
63 | 75 |
|
64 | 76 |
|
65 | | -def _read(visit_dir, logger) -> tuple[Path, dict | None]: |
66 | | - """A visit config file's path and parsed contents, the latter None if the |
67 | | - file is there but could not be read.""" |
68 | | - path = Path(visit_dir) / CONFIG_FILENAME |
| 77 | +def _read(path: Path, logger) -> dict | None: |
| 78 | + """A config file's parsed contents, or None if it is there but unreadable.""" |
69 | 79 | try: |
70 | 80 | text = path.read_text() if path.is_file() else "" |
71 | | - return path, _mapping(yaml.safe_load(text)) |
| 81 | + return _mapping(yaml.safe_load(text)) |
72 | 82 | except Exception as e: |
73 | 83 | logger.warning(f"Ignoring unreadable visit config {path}: {e}") |
74 | | - return path, None |
| 84 | + return None |
75 | 85 |
|
76 | 86 |
|
77 | 87 | def load_visit_config(visit_dir, logger) -> VisitConfig: |
78 | | - """Read a visit's config file, or an all-defaults config if it has none. |
| 88 | + """Read a visit's settings and cached acronym, or an all-defaults config. |
79 | 89 |
|
80 | 90 | A setting that fails validation is dropped on its own and the rest of the |
81 | 91 | file still applies, so one typo cannot cost a visit its whole config. |
82 | 92 | """ |
83 | | - path, raw = _read(visit_dir, logger) |
84 | | - raw = raw or {} |
85 | | - values = {str(k): v for k, v in _mapping(raw.get("autoprocessing")).items()} |
86 | | - values["acronym"] = _mapping(raw.get("data")).get("acronym") |
87 | | - values["notify"] = raw.get("notify") |
| 93 | + visit_dir = Path(visit_dir) |
| 94 | + cache_path = visit_dir / CACHE_FILE |
| 95 | + cache = _read(cache_path, logger) or {} |
| 96 | + |
| 97 | + # visits predating the split keep their settings in the cache file |
| 98 | + settings_path = visit_dir / SETTINGS_FILE |
| 99 | + if settings_path.is_file(): |
| 100 | + settings = _read(settings_path, logger) or {} |
| 101 | + else: |
| 102 | + settings_path, settings = cache_path, cache |
| 103 | + |
| 104 | + values = {str(k): v for k, v in _mapping(settings.get("autoprocessing")).items()} |
| 105 | + values["acronym"] = _mapping(cache.get("data")).get("acronym") |
| 106 | + values["notify"] = settings.get("notify") |
88 | 107 | try: |
89 | 108 | return VisitConfig(**values) |
90 | 109 | except pydantic.ValidationError as e: |
91 | 110 | # only return settings that validate |
92 | 111 | bad = {str(err["loc"][0]) for err in e.errors() if err["loc"]} |
93 | | - logger.warning(f"Ignoring invalid {', '.join(sorted(bad))} in {path}: {e}") |
| 112 | + logger.warning( |
| 113 | + f"Ignoring invalid {', '.join(sorted(bad))} in {settings_path}: {e}" |
| 114 | + ) |
94 | 115 | return VisitConfig(**{k: v for k, v in values.items() if k not in bad}) |
95 | 116 |
|
96 | 117 |
|
97 | 118 | def cache_acronym(visit_dir, acronym: str, logger) -> None: |
98 | | - """Cache a visit's target acronym to its config file so that a beamline visit |
99 | | - can be linked with a labxchem one. |
| 119 | + """Cache a visit's target acronym so that a beamline visit can be linked |
| 120 | + with a labxchem one. Does nothing if an acronym is already recorded. |
| 121 | +
|
| 122 | + Written at the top of the visit, not under `processing`, because that is |
| 123 | + where the pipeline can write. |
100 | 124 | """ |
101 | | - path, raw = _read(visit_dir, logger) |
| 125 | + path = Path(visit_dir) / CACHE_FILE |
| 126 | + raw = _read(path, logger) |
102 | 127 | if raw is None: |
103 | 128 | return |
104 | 129 | data = _mapping(raw.get("data")) |
|
0 commit comments