77import logging
88from datetime import datetime
99from pathlib import Path
10+ from typing import Any , Literal
1011
1112import fsspec
1213import jsonpickle
@@ -98,13 +99,14 @@ def publish_files(
9899
99100
100101class 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