|
4 | 4 | """ |
5 | 5 |
|
6 | 6 | import os |
| 7 | +import re |
7 | 8 | import sys |
8 | 9 | from copy import deepcopy |
9 | 10 | from datetime import datetime |
@@ -54,7 +55,47 @@ def get_ledger(): |
54 | 55 | return current_ledger |
55 | 56 |
|
56 | 57 |
|
57 | | -def apply_page(file, event=None, ledger=None, update_page=False): |
| 58 | +def _raw_production_names(ledger, event_name): |
| 59 | + """Return the set of production names for an event from raw ledger data. |
| 60 | +
|
| 61 | + Reads directly from ``ledger.events`` (a plain dict) rather than |
| 62 | + constructing a full ``Event`` object, avoiding expensive git and |
| 63 | + Production initialisation just to obtain a set of strings. |
| 64 | + """ |
| 65 | + names = set() |
| 66 | + for prod in ledger.events.get(event_name, {}).get("productions", []): |
| 67 | + if isinstance(prod, dict) and len(prod) == 1: |
| 68 | + names.add(next(iter(prod))) |
| 69 | + elif isinstance(prod, dict) and "name" in prod: |
| 70 | + names.add(prod["name"]) |
| 71 | + return names |
| 72 | + |
| 73 | + |
| 74 | +def next_available_name(name, existing_names): |
| 75 | + """Return the next available analysis name, incrementing a numeric suffix if needed. |
| 76 | +
|
| 77 | + If ``name`` is not already taken, it is returned unchanged. Otherwise the |
| 78 | + trailing ``-N`` suffix (if present) is stripped to find the stem, and the |
| 79 | + lowest integer >= 2 that produces a free name is appended. |
| 80 | +
|
| 81 | + Examples |
| 82 | + -------- |
| 83 | + >>> next_available_name("bilby-IMRPhenomXPHM", {"bilby-IMRPhenomXPHM"}) |
| 84 | + 'bilby-IMRPhenomXPHM-2' |
| 85 | + >>> next_available_name("bilby-IMRPhenomXPHM-2", {"bilby-IMRPhenomXPHM-2"}) |
| 86 | + 'bilby-IMRPhenomXPHM-3' |
| 87 | + """ |
| 88 | + if name not in existing_names: |
| 89 | + return name |
| 90 | + match = re.match(r"^(.*)-(\d+)$", name) |
| 91 | + stem, n = (match.group(1), int(match.group(2))) if match else (name, 1) |
| 92 | + n += 1 |
| 93 | + while f"{stem}-{n}" in existing_names: |
| 94 | + n += 1 |
| 95 | + return f"{stem}-{n}" |
| 96 | + |
| 97 | + |
| 98 | +def apply_page(file, event=None, ledger=None, update_page=False, name=None, iterate=False): |
58 | 99 | # Get ledger if not provided |
59 | 100 | if ledger is None: |
60 | 101 | ledger = get_ledger() |
@@ -121,10 +162,10 @@ def apply_page(file, event=None, ledger=None, update_page=False): |
121 | 162 | history[version]["date changed"] = datetime.now() |
122 | 163 |
|
123 | 164 | ledger.data["history"][event_obj.name] = history |
124 | | - ledger.save() |
125 | 165 | update(ledger.events[event_obj.name], event_obj.meta) |
126 | 166 | ledger.events[event_obj.name]["productions"] = analyses |
127 | 167 | ledger.events[event_obj.name].pop("ledger", None) |
| 168 | + ledger.save() |
128 | 169 |
|
129 | 170 | click.echo( |
130 | 171 | click.style("●", fg="green") + f" Successfully updated {event_obj.name}" |
@@ -169,33 +210,46 @@ def apply_page(file, event=None, ledger=None, update_page=False): |
169 | 210 | prompt = "Which event should these be applied to?" |
170 | 211 | event_s = str(click.prompt(prompt)) |
171 | 212 |
|
| 213 | + # Resolve name overrides before constructing the Event object. |
| 214 | + # Reading from the raw ledger dict is cheap; get_event() is expensive |
| 215 | + # (it instantiates Production objects and runs git/graph operations). |
| 216 | + if name is not None or iterate: |
| 217 | + existing_names = _raw_production_names(ledger, event_s) |
172 | 218 | for expanded_doc in expanded_documents: |
173 | | - |
174 | | - try: |
175 | | - event_obj = ledger.get_event(event_s)[0] |
176 | | - except KeyError as e: |
177 | | - click.echo( |
178 | | - click.style("●", fg="red") |
179 | | - + f" Could not apply a production, couldn't find the event {event}" |
180 | | - ) |
181 | | - logger.exception(e) |
182 | | - production = asimov.event.Production.from_dict( |
183 | | - parameters=expanded_doc, subject=event_obj, ledger=ledger |
184 | | - ) |
185 | | - try: |
186 | | - ledger.add_analysis(production, event=event_obj) |
187 | | - click.echo( |
188 | | - click.style("●", fg="green") |
189 | | - + f" Successfully applied {production.name} to {event_obj.name}" |
190 | | - ) |
191 | | - logger.info(f"Added {production.name} to {event_obj.name}") |
192 | | - except ValueError as e: |
193 | | - click.echo( |
194 | | - click.style("●", fg="red") |
195 | | - + f" Could not apply {production.name} to {event_obj.name} as " |
196 | | - + "an analysis already exists with this name" |
197 | | - ) |
198 | | - logger.exception(e) |
| 219 | + if name is not None: |
| 220 | + expanded_doc["name"] = name |
| 221 | + elif iterate: |
| 222 | + expanded_doc["name"] = next_available_name(expanded_doc["name"], existing_names) |
| 223 | + # Keep existing_names current so consecutive iterations in a |
| 224 | + # strategy expansion don't collide with each other. |
| 225 | + existing_names.add(expanded_doc["name"]) |
| 226 | + |
| 227 | + try: |
| 228 | + event_obj = ledger.get_event(event_s)[0] |
| 229 | + except KeyError as e: |
| 230 | + click.echo( |
| 231 | + click.style("●", fg="red") |
| 232 | + + f" Could not apply a production, couldn't find the event {event}" |
| 233 | + ) |
| 234 | + logger.exception(e) |
| 235 | + continue |
| 236 | + production = asimov.event.Production.from_dict( |
| 237 | + parameters=expanded_doc, subject=event_obj, ledger=ledger |
| 238 | + ) |
| 239 | + try: |
| 240 | + ledger.add_analysis(production, event=event_obj) |
| 241 | + click.echo( |
| 242 | + click.style("●", fg="green") |
| 243 | + + f" Successfully applied {production.name} to {event_obj.name}" |
| 244 | + ) |
| 245 | + logger.info(f"Added {production.name} to {event_obj.name}") |
| 246 | + except ValueError as e: |
| 247 | + click.echo( |
| 248 | + click.style("●", fg="red") |
| 249 | + + f" Could not apply {production.name} to {event_obj.name} as " |
| 250 | + + "an analysis already exists with this name" |
| 251 | + ) |
| 252 | + logger.exception(e) |
199 | 253 |
|
200 | 254 | elif document["kind"].lower() == "postprocessing": |
201 | 255 | # Handle a project analysis |
@@ -436,11 +490,24 @@ def apply_via_plugin(event, hookname, **kwargs): |
436 | 490 | default=False, |
437 | 491 | help="Update the project with this blueprint rather than adding a new record.", |
438 | 492 | ) |
439 | | -def apply(file, event, plugin, update): |
| 493 | +@click.option( |
| 494 | + "--name", |
| 495 | + "-n", |
| 496 | + default=None, |
| 497 | + help="Override the analysis name specified in the blueprint.", |
| 498 | +) |
| 499 | +@click.option( |
| 500 | + "--iterate", |
| 501 | + "-I", |
| 502 | + is_flag=True, |
| 503 | + default=False, |
| 504 | + help="Automatically increment the analysis name suffix to avoid a name conflict.", |
| 505 | +) |
| 506 | +def apply(file, event, plugin, update, name, iterate): |
440 | 507 | from asimov import setup_file_logging |
441 | 508 | current_ledger = get_ledger() |
442 | 509 | setup_file_logging() |
443 | 510 | if plugin: |
444 | 511 | apply_via_plugin(event, hookname=plugin) |
445 | 512 | elif file: |
446 | | - apply_page(file, event, ledger=current_ledger, update_page=update) |
| 513 | + apply_page(file, event, ledger=current_ledger, update_page=update, name=name, iterate=iterate) |
0 commit comments