Skip to content

Commit f90e827

Browse files
committed
Updated publisher logic to support multi mode parameter
1 parent 6229b88 commit f90e827

6 files changed

Lines changed: 211 additions & 98 deletions

File tree

CHANGES.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,11 @@
3333
- Introduced build_link_to_jnb method for creating STAC-compatible notebook links with
3434
metadata on kernel, environment, and containerization.
3535
- Added originating application platform metadata to generated OGC API records for
36-
DeepESDL experiments and workflows.
36+
DeepESDL experiments and workflows.
37+
38+
## Changes in 0.1.6
39+
40+
- Publisher now supports `mode` parameter, This allows more flexible publishing:
41+
- `"dataset"` → publish dataset only
42+
- `"workflow"` → publish workflow only
43+
- `"all"` → publish both (default)

deep_code/cli/publish.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,43 @@
44
# Permissions are hereby granted under the terms of the MIT License:
55
# https://opensource.org/licenses/MIT.
66

7+
from pathlib import Path
8+
79
import click
810

911
from deep_code.tools.publish import Publisher
1012

1113

14+
def _validate_inputs(dataset_config, workflow_config, mode):
15+
mode = mode.lower()
16+
17+
def ensure_file(path: str, label: str):
18+
if path is None:
19+
raise click.UsageError(f"{label} is required but was not provided.")
20+
if not Path(path).is_file():
21+
raise click.UsageError(f"{label} not found: {path}")
22+
23+
if mode == "dataset":
24+
# Need dataset only
25+
ensure_file(dataset_config, "DATASET_CONFIG")
26+
if workflow_config is not None:
27+
click.echo("ℹ️ Ignoring WORKFLOW_CONFIG since mode=dataset.", err=True)
28+
29+
elif mode == "workflow":
30+
# Need workflow config only
31+
ensure_file(workflow_config, "WORKFLOW_CONFIG")
32+
33+
elif mode == "all":
34+
# Need both
35+
ensure_file(dataset_config, "DATASET_CONFIG")
36+
ensure_file(workflow_config, "WORKFLOW_CONFIG")
37+
38+
else:
39+
raise click.UsageError(
40+
"Invalid mode. Choose one of: all, dataset, workflow_experiment."
41+
)
42+
43+
1244
@click.command(name="publish")
1345
@click.argument("dataset_config", type=click.Path(exists=True))
1446
@click.argument("workflow_config", type=click.Path(exists=True))
@@ -19,7 +51,14 @@
1951
default="production",
2052
help="Target environment for publishing (production, staging, testing)",
2153
)
22-
def publish(dataset_config, workflow_config, environment):
54+
@click.option(
55+
"--mode",
56+
"-m",
57+
type=click.Choice(["all", "dataset", "workflow"], case_sensitive=False),
58+
default="all",
59+
help="Publishing mode: dataset only, workflow only, or both",
60+
)
61+
def publish(dataset_config, workflow_config, environment, mode):
2362
"""Request publishing a dataset along with experiment and workflow metadata to the
2463
open science catalogue.
2564
"""
@@ -28,4 +67,5 @@ def publish(dataset_config, workflow_config, environment):
2867
workflow_config_path=workflow_config,
2968
environment=environment.lower(),
3069
)
31-
publisher.publish_all()
70+
result = publisher.publish(mode=mode.lower())
71+
click.echo(f"Pull request created: {result}")

deep_code/tools/publish.py

Lines changed: 150 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
from datetime import datetime
99
from pathlib import Path
10+
from typing import Any, Literal
1011

1112
import fsspec
1213
import jsonpickle
@@ -98,13 +99,14 @@ def publish_files(
9899

99100

100101
class Publisher:
101-
"""Publishes products (datasets) to the OSC GitHub repository.
102+
"""Publishes products (datasets), workflows and experiments to the OSC GitHub
103+
repository.
102104
"""
103105

104106
def __init__(
105107
self,
106-
dataset_config_path: str,
107-
workflow_config_path: str,
108+
dataset_config_path: str | None = None,
109+
workflow_config_path: str | None = None,
108110
environment: str = "production",
109111
):
110112
self.environment = environment
@@ -121,24 +123,37 @@ def __init__(
121123
self.collection_id = ""
122124
self.workflow_title = ""
123125

124-
# Paths to configuration files
126+
# Paths to configuration files, may be optional
125127
self.dataset_config_path = dataset_config_path
126128
self.workflow_config_path = workflow_config_path
127129

130+
# Config dicts (loaded lazily)
131+
self.dataset_config: dict[str, Any] = {}
132+
self.workflow_config: dict[str, Any] = {}
133+
134+
# Values that may be set from configs (lazy)
135+
self.collection_id: str | None = None
136+
self.workflow_title: str | None = None
137+
self.workflow_id: str | None = None
138+
128139
# Load configuration files
129140
self._read_config_files()
130-
self.collection_id = self.dataset_config.get("collection_id")
131-
self.workflow_title = self.workflow_config.get("properties", {}).get("title")
132-
self.workflow_id = self.workflow_config.get("workflow_id")
133141

134-
if not self.collection_id:
135-
raise ValueError("collection_id is missing in dataset config.")
142+
if self.dataset_config:
143+
self.collection_id = self.dataset_config.get("collection_id")
144+
if self.workflow_config:
145+
self.workflow_title = self.workflow_config.get("properties", {}).get(
146+
"title"
147+
)
148+
self.workflow_id = self.workflow_config.get("workflow_id")
136149

137150
def _read_config_files(self) -> None:
138-
with fsspec.open(self.dataset_config_path, "r") as file:
139-
self.dataset_config = yaml.safe_load(file) or {}
140-
with fsspec.open(self.workflow_config_path, "r") as file:
141-
self.workflow_config = yaml.safe_load(file) or {}
151+
if self.dataset_config_path:
152+
with fsspec.open(self.dataset_config_path, "r") as file:
153+
self.dataset_config = yaml.safe_load(file) or {}
154+
if self.workflow_config_path:
155+
with fsspec.open(self.workflow_config_path, "r") as file:
156+
self.workflow_config = yaml.safe_load(file) or {}
142157

143158
@staticmethod
144159
def _write_to_file(file_path: str, data: dict):
@@ -206,10 +221,18 @@ def _update_variable_catalogs(self, generator, file_dict, variable_ids):
206221
)
207222
file_dict[var_file_path] = updated_catalog.to_dict()
208223

209-
def publish_dataset(self, write_to_file: bool = False):
224+
def publish_dataset(
225+
self,
226+
write_to_file: bool = False,
227+
mode: Literal["all", "dataset", "workflow"] = "all",
228+
) -> dict[str, Any]:
210229
"""Prepare dataset/product collection for publishing to the specified GitHub
211230
repository."""
212231

232+
if not self.dataset_config:
233+
raise ValueError(
234+
"No dataset config loaded. Provide dataset_config_path to publish dataset."
235+
)
213236
dataset_id = self.dataset_config.get("dataset_id")
214237
self.collection_id = self.dataset_config.get("collection_id")
215238
documentation_link = self.dataset_config.get("documentation_link")
@@ -240,7 +263,7 @@ def publish_dataset(self, write_to_file: bool = False):
240263
)
241264

242265
variable_ids = generator.get_variable_ids()
243-
ds_collection = generator.build_dataset_stac_collection()
266+
ds_collection = generator.build_dataset_stac_collection(mode=mode)
244267

245268
# Prepare a dictionary of file paths and content
246269
file_dict = {}
@@ -315,9 +338,19 @@ def _update_base_catalog(
315338

316339
return base_catalog
317340

318-
def generate_workflow_experiment_records(self, write_to_file: bool = False) -> None:
341+
def generate_workflow_experiment_records(
342+
self,
343+
write_to_file: bool = False,
344+
mode: Literal["all", "dataset", "workflow"] = "all",
345+
) -> dict[str, Any]:
319346
"""prepare workflow and experiment as ogc api record to publish it to the
320347
specified GitHub repository."""
348+
349+
file_dict = {}
350+
351+
if mode not in {"workflow", "all"}:
352+
return file_dict # nothing to do for mode="dataset"
353+
321354
workflow_id = self._normalize_name(self.workflow_config.get("workflow_id"))
322355
if not workflow_id:
323356
raise ValueError("workflow_id is missing in workflow config.")
@@ -367,85 +400,117 @@ def generate_workflow_experiment_records(self, write_to_file: bool = False) -> N
367400
exp_record_properties.type = "experiment"
368401
exp_record_properties.osc_workflow = workflow_id
369402

370-
dataset_link = link_builder.build_link_to_dataset(self.collection_id)
371-
372-
experiment_record = ExperimentAsOgcRecord(
373-
id=workflow_id,
374-
title=self.workflow_title,
375-
type="Feature",
376-
jupyter_notebook_url=jupyter_notebook_url,
377-
collection_id=self.collection_id,
378-
properties=exp_record_properties,
379-
links=links + theme_links + dataset_link,
380-
)
381-
# Convert to dictionary and cleanup
382-
experiment_dict = experiment_record.to_dict()
383-
if "jupyter_notebook_url" in experiment_dict:
384-
del experiment_dict["jupyter_notebook_url"]
385-
if "collection_id" in experiment_dict:
386-
del experiment_dict["collection_id"]
387-
if "osc:project" in experiment_dict["properties"]:
388-
del experiment_dict["properties"]["osc:project"]
389-
# add experiment record to file_dict
390-
exp_file_path = f"experiments/{workflow_id}/record.json"
391-
file_dict[exp_file_path] = experiment_dict
392-
393-
# Update base catalogs of experiments and workflows with links
394-
file_dict["experiments/catalog.json"] = self._update_base_catalog(
395-
catalog_path="experiments/catalog.json",
396-
item_id=workflow_id,
397-
self_href=EXPERIMENT_BASE_CATALOG_SELF_HREF,
398-
)
403+
if mode in ["all"]:
404+
if not getattr(self, "collection_id", None):
405+
raise ValueError(
406+
"collection_id is required to generate the experiment record when mode='all' "
407+
"(the experiment links to the output dataset)."
408+
)
409+
# generate experiment record only if there is an output dataset
410+
dataset_link = link_builder.build_link_to_dataset(self.collection_id)
411+
412+
experiment_record = ExperimentAsOgcRecord(
413+
id=workflow_id,
414+
title=self.workflow_title,
415+
type="Feature",
416+
jupyter_notebook_url=jupyter_notebook_url,
417+
collection_id=self.collection_id,
418+
properties=exp_record_properties,
419+
links=links + theme_links + dataset_link,
420+
)
421+
# Convert to dictionary and cleanup
422+
experiment_dict = experiment_record.to_dict()
423+
if "jupyter_notebook_url" in experiment_dict:
424+
del experiment_dict["jupyter_notebook_url"]
425+
if "collection_id" in experiment_dict:
426+
del experiment_dict["collection_id"]
427+
if "osc:project" in experiment_dict["properties"]:
428+
del experiment_dict["properties"]["osc:project"]
429+
# add experiment record to file_dict
430+
exp_file_path = f"experiments/{workflow_id}/record.json"
431+
file_dict[exp_file_path] = experiment_dict
432+
433+
# Update base catalogs of experiments and workflows with links
434+
file_dict["experiments/catalog.json"] = self._update_base_catalog(
435+
catalog_path="experiments/catalog.json",
436+
item_id=workflow_id,
437+
self_href=EXPERIMENT_BASE_CATALOG_SELF_HREF,
438+
)
399439

400-
file_dict["workflows/catalog.json"] = self._update_base_catalog(
401-
catalog_path="workflows/catalog.json",
402-
item_id=workflow_id,
403-
self_href=WORKFLOW_BASE_CATALOG_SELF_HREF,
404-
)
440+
file_dict["workflows/catalog.json"] = self._update_base_catalog(
441+
catalog_path="workflows/catalog.json",
442+
item_id=workflow_id,
443+
self_href=WORKFLOW_BASE_CATALOG_SELF_HREF,
444+
)
405445
# Write to files if testing
406446
if write_to_file:
407447
for file_path, data in file_dict.items():
408448
self._write_to_file(file_path, data)
409449
return {}
410450
return file_dict
411451

412-
def publish_all(self, write_to_file: bool = False):
413-
"""Publish both dataset and workflow/experiment in a single PR."""
414-
# Get file dictionaries from both methods
415-
dataset_files = self.publish_dataset(write_to_file=write_to_file)
416-
workflow_files = self.generate_workflow_experiment_records(
417-
write_to_file=write_to_file
418-
)
452+
def publish(
453+
self,
454+
write_to_file: bool = False,
455+
mode: Literal["all", "dataset", "workflow"] = "all",
456+
) -> dict[str, Any] | str:
457+
"""
458+
Publish both dataset and workflow/experiment in a single PR.
459+
Args:
460+
write_to_file: If True, write JSON files locally and return the generated dict(s).
461+
If False, open a PR and return the PR URL.
462+
mode: Select which artifacts to publish:
463+
- "dataset": only dataset collection & related catalogs
464+
- "workflow": only workflow records
465+
- "all": both
466+
Returns:
467+
dict[str, Any] when write_to_file=True (the files written),
468+
or str when write_to_file=False (the PR URL).
469+
"""
419470

420-
# Combine the file dictionaries
421-
combined_files = {**dataset_files, **workflow_files}
471+
files: dict[str, Any] = {}
422472

423-
if not write_to_file:
424-
# Create branch name, commit message, PR info
425-
branch_name = (
426-
f"{OSC_BRANCH_NAME}-{self.collection_id}"
427-
f"-{datetime.now().strftime('%Y%m%d%H%M%S')}"
428-
)
429-
commit_message = (
430-
f"Add new dataset collection: {self.collection_id} and "
431-
f"workflow/experiment: {self.workflow_config.get('workflow_id')}"
432-
)
433-
pr_title = (
434-
f"Add new dataset collection: {self.collection_id} and "
435-
f"workflow/experiment: {self.workflow_config.get('workflow_id')}"
436-
)
437-
pr_body = (
438-
f"This PR adds a new dataset collection: {self.collection_id} and "
439-
f"its corresponding workflow/experiment to the repository."
440-
)
473+
if mode in ("dataset", "all"):
474+
ds_files = self.publish_dataset(write_to_file=False, mode=mode)
475+
files.update(ds_files)
441476

442-
# Publish all files in one go
443-
pr_url = self.gh_publisher.publish_files(
444-
branch_name=branch_name,
445-
file_dict=combined_files,
446-
commit_message=commit_message,
447-
pr_title=pr_title,
448-
pr_body=pr_body,
477+
if mode in ("workflow", "all"):
478+
wf_files = self.generate_workflow_experiment_records(write_to_file=False)
479+
files.update(wf_files)
480+
481+
if not files:
482+
raise ValueError(
483+
"Nothing to publish. Choose mode='dataset', 'workflow', or 'all'."
449484
)
450485

451-
logger.info(f"Pull request created: {pr_url}")
486+
if write_to_file:
487+
for file_path, data in files.items():
488+
# file_path might be a Path (from _update_and_add_to_file_dict) – normalize to str
489+
out_path = str(file_path)
490+
self._write_to_file(out_path, data)
491+
return {} # consistent with existing write_to_file behavior
492+
493+
# Prepare PR
494+
mode_label = {
495+
"dataset": f"dataset: {self.collection_id or 'unknown'}",
496+
"workflow": f"workflow: {self.workflow_id or 'unknown'}",
497+
"all": f"dataset: {self.collection_id or 'unknown'} + workflow/experiment: {self.workflow_id or 'unknown'}",
498+
}[mode]
499+
500+
branch_name = (
501+
f"{OSC_BRANCH_NAME}-{(self.collection_id or self.workflow_id or 'osc')}"
502+
f"-{datetime.now().strftime('%Y%m%d%H%M%S')}"
503+
)
504+
commit_message = f"Publish {mode_label}"
505+
pr_title = f"Publish {mode_label}"
506+
pr_body = f"This PR publishes {mode_label} to the repository."
507+
508+
pr_url = self.gh_publisher.publish_files(
509+
branch_name=branch_name,
510+
file_dict=files,
511+
commit_message=commit_message,
512+
pr_title=pr_title,
513+
pr_body=pr_body,
514+
)
515+
logger.info(f"Pull request created: {pr_url}")
516+
return pr_url

0 commit comments

Comments
 (0)