3434
3535from backend .config import PARAMETER_SOURCE_MAP , WATERLEVELS
3636from backend .persisters .ogc_features import (
37+ dump_major_chemistry_collection ,
3738 dump_summary_collection ,
3839 dump_timeseries_collection ,
3940)
4748_CHECK_NAME = "returned_data"
4849_GEOSERVER_CHECK_NAME = "registered"
4950
51+ # Classic major-ion suite for the ogc_major_chemistry product. One feature per
52+ # well, with each analyte's latest value/units/date as properties.
53+ _MAJOR_CHEMISTRY = [
54+ "calcium" ,
55+ "magnesium" ,
56+ "sodium" ,
57+ "potassium" ,
58+ "bicarbonate" ,
59+ "carbonate" ,
60+ "chloride" ,
61+ "sulfate" ,
62+ ]
63+
64+
65+ def _product_params (product : dict ) -> list :
66+ """The DIE parameter(s) a product unifies. Single-parameter products yield
67+ one; the major-chemistry product yields the major-ion suite."""
68+ if product .get ("output_type" ) == "ogc_major_chemistry" :
69+ return list (_MAJOR_CHEMISTRY )
70+ return [product ["parameter" ]]
71+
5072
5173def _product_source_keys (product : dict ) -> list :
52- """Source keys that apply to this product: the parameter's agencies,
53- filtered by the product's include/exclude list."""
54- agencies = PARAMETER_SOURCE_MAP [product ["parameter" ]]["agencies" ]
74+ """Source keys that apply to this product: the union of its parameters'
75+ agencies, filtered by the product's include/exclude list."""
76+ agencies : list = []
77+ for param in _product_params (product ):
78+ for a in PARAMETER_SOURCE_MAP [param ]["agencies" ]:
79+ if a not in agencies :
80+ agencies .append (a )
5581 spec = product .get ("sources" , {}) or {}
5682 if spec .get ("include" ):
5783 return [a for a in agencies if a in spec ["include" ]]
5884 if spec .get ("exclude" ):
5985 return [a for a in agencies if a not in spec ["exclude" ]]
60- return list ( agencies )
86+ return agencies
6187
6288
6389def _in_name (source_key : str ) -> str :
@@ -75,24 +101,32 @@ def _build_source_asset(product: dict, source_key: str, group: str):
75101 plain ``_payload`` dicts for IO-manager pickling (see module docstring)."""
76102 pid = product ["id" ]
77103 src_key = dg .AssetKey ([pid , "sources" , source_key ])
104+ params = _product_params (product )
78105
79106 @dg .asset (
80107 key = src_key ,
81108 group_name = group ,
82109 check_specs = [dg .AssetCheckSpec (name = _CHECK_NAME , asset = src_key )],
83110 )
84111 def _source_asset (context : dg .AssetExecutionContext , die_config : DIEConfigResource ):
85- config = die_config .get_config (product )
86-
87112 error = ""
88113 records , sites , timeseries = [], [], []
89114 try :
115+ # One unification pass per parameter. Single-parameter products run
116+ # once; the major-chemistry product runs once per analyte and
117+ # accumulates summary records (analyte identity lives in each
118+ # record's parameter_name). A source that doesn't provide a given
119+ # parameter is skipped by unify_source (source_pair → None).
90120 with forward_die_logs (context ):
91- persister = unify_source (config , source_key )
92- # Ship plain dicts across the IO manager; rebuild in combine.
93- records = [r ._payload for r in persister .records ]
94- sites = [s ._payload for s in persister .sites ]
95- timeseries = [[o ._payload for o in site_ts ] for site_ts in persister .timeseries ]
121+ for param in params :
122+ config = die_config .get_config (product , parameter = param )
123+ persister = unify_source (config , source_key )
124+ # Ship plain dicts across the IO manager; rebuild in combine.
125+ records .extend (r ._payload for r in persister .records )
126+ sites .extend (s ._payload for s in persister .sites )
127+ timeseries .extend (
128+ [o ._payload for o in site_ts ] for site_ts in persister .timeseries
129+ )
96130 except Exception :
97131 error = traceback .format_exc ()
98132 context .log .error (f"Source { source_key } failed:\n { error } " )
@@ -132,10 +166,11 @@ def _build_combine_asset(product: dict, source_keys: list, source_asset_keys: li
132166 """Build the combine asset (keyed ``[product_id]``) for *product*.
133167
134168 Depends on every source asset (wired via ``ins``), merges their
135- records/sites/timeseries, writes the OGC GeoJSON collection — summary or
136- timeseries depending on ``output_type`` — and uploads it to GCS."""
169+ records/sites/timeseries, writes the OGC GeoJSON collection — summary,
170+ timeseries, or major-chemistry depending on ``output_type`` — and uploads it
171+ to GCS."""
137172 pid = product ["id" ]
138- is_summary = product ["output_type" ] == "ogc_summary"
173+ output_type = product ["output_type" ]
139174 ins = {
140175 _in_name (k ): dg .AssetIn (key = ak )
141176 for k , ak in zip (source_keys , source_asset_keys )
@@ -161,7 +196,12 @@ def _combine_asset(
161196
162197 with tempfile .TemporaryDirectory () as tmpdir :
163198 out = Path (tmpdir ) / "collection.geojson"
164- if is_summary :
199+ if output_type == "ogc_major_chemistry" :
200+ # All summary records (one per well+analyte); the dumper pivots
201+ # to one feature per well with analytes as properties.
202+ records = [SummaryRecord (p ) for p in all_records ]
203+ dump_major_chemistry_collection (str (out ), records , meta )
204+ elif output_type == "ogc_summary" :
165205 records = [SummaryRecord (p ) for p in all_records ]
166206 dump_summary_collection (str (out ), records , meta )
167207 else :
@@ -275,8 +315,9 @@ def build_product_assets(product: dict) -> list:
275315 """Return the full asset list for *product*: one source asset per applicable
276316 source, the combine asset, and the geoserver publish asset (see module
277317 docstring for the graph shape). Assets are grouped ``waterlevels`` or
278- ``analytes`` by parameter."""
279- group = "waterlevels" if product ["parameter" ] == WATERLEVELS else "analytes"
318+ ``analytes`` by parameter (major-chemistry products group under
319+ ``analytes``)."""
320+ group = "waterlevels" if product .get ("parameter" ) == WATERLEVELS else "analytes"
280321 source_keys = _product_source_keys (product )
281322
282323 source_assets = []
0 commit comments