From e89d3eb3f085cdb1dd3afd0639aa3bfcf3f99a98 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 06:40:28 -0600 Subject: [PATCH 01/16] feat: update max capacity with rating props for generators --- .../r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 023b0143..6be0287c 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -1522,11 +1522,6 @@ def _apply_small_capacity_default(capacity_mw: float) -> float: if default_max > 0.0: return round(default_max, 2) - - # Final safeguard: if the resolved category has no usable defaults - # (common for some hydro/renewable mappings), fall back to a stable - # generic thermal max-capacity baseline so tiny p.u.-like values don't - # leak into PLEXOS max_capacity/min_stable_level. generic_default = _get_defaults("gas-cc", "max_capacity_MW") or _get_defaults("gas-cc", "capacity_MW") if generic_default > 0.0: return round(generic_default, 2) From 85103f71c3be484cfc3dc020f340fc3d6af5d9d4 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 06:42:51 -0600 Subject: [PATCH 02/16] fix: change rules file sienna regions to areas ref --- .../src/r2x_sienna_to_plexos/config/rules.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index 4356eac0..3783800e 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -21,7 +21,7 @@ }, { "defaults": { - "category": "sienna-regions" + "category": "areas" }, "field_map": { "uuid": "uuid", From ba829e77856adad284d605a339b80a9fb8066932 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 06:45:27 -0600 Subject: [PATCH 03/16] fix: remove acpf related properties from rules file --- .../r2x_sienna_to_plexos/config/rules.json | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index 3783800e..f2bbad2e 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -198,11 +198,9 @@ { "defaults": { "category": "tap-transformer", - "ac_tap_ratio": 1.0, "susceptance": 0.0 }, "field_map": { - "ac_tap_ratio": "tap", "name": "name", "reactance": "x", "resistance": "r", @@ -221,7 +219,6 @@ { "defaults": { "category": "phase-shifting-transformer", - "ac_tap_ratio": 1.0, "susceptance": 0.0 }, "field_map": { @@ -229,9 +226,7 @@ "uuid": "uuid", "category": "category", "reactance": "x", - "resistance": "r", - "ac_tap_ratio": "tap", - "ac_fixed_shift_angle": "α" + "resistance": "r" }, "getters": { "units": "get_availability", @@ -250,8 +245,7 @@ }, "field_map": { "reactance": "x_primary", - "resistance": "r_primary", - "ac_tap_ratio": "primary_turns_ratio" + "resistance": "r_primary" }, "getters": { "name": "get_3w_transformer_primary_name", @@ -272,8 +266,7 @@ }, "field_map": { "reactance": "x_secondary", - "resistance": "r_secondary", - "ac_tap_ratio": "secondary_turns_ratio" + "resistance": "r_secondary" }, "getters": { "name": "get_3w_transformer_secondary_name", @@ -293,8 +286,7 @@ }, "field_map": { "reactance": "x_tertiary", - "resistance": "r_tertiary", - "ac_tap_ratio": "tertiary_turns_ratio" + "resistance": "r_tertiary" }, "getters": { "name": "get_3w_transformer_tertiary_name", @@ -310,14 +302,11 @@ "defaults": { "category": "pst-trf3w-primary-winding", "units": 1, - "ac_tap_ratio": 1.0, "susceptance": 0.0 }, "field_map": { "reactance": "x_primary", - "resistance": "r_primary", - "ac_tap_ratio": "primary_turns_ratio", - "ac_fixed_shift_angle": "α_primary" + "resistance": "r_primary" }, "getters": { "name": "get_3w_transformer_primary_name", @@ -333,14 +322,11 @@ "defaults": { "category": "pst-trf3w-secondary-winding", "units": 1, - "ac_tap_ratio": 1.0, "susceptance": 0.0 }, "field_map": { "reactance": "x_secondary", - "resistance": "r_secondary", - "ac_tap_ratio": "secondary_turns_ratio", - "ac_fixed_shift_angle": "α_secondary" + "resistance": "r_secondary" }, "getters": { "name": "get_3w_transformer_secondary_name", @@ -356,14 +342,11 @@ "defaults": { "category": "pst-trf3w-tertiary-winding", "units": 1, - "ac_tap_ratio": 1.0, "susceptance": 0.0 }, "field_map": { "reactance": "x_tertiary", - "resistance": "r_tertiary", - "ac_tap_ratio": "tertiary_turns_ratio", - "ac_fixed_shift_angle": "α_tertiary" + "resistance": "r_tertiary" }, "getters": { "name": "get_3w_transformer_tertiary_name", From ff533e607e12a1aa8f25da3b1096ab070a35af41 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 07:42:51 -0600 Subject: [PATCH 04/16] fix: match gen categories correctly by thermal fuel and remove syn-cond category --- .../r2x_sienna_to_plexos/config/defaults.json | 62 ++++- .../r2x_sienna_to_plexos/config/rules.json | 26 --- .../src/r2x_sienna_to_plexos/getters.py | 217 +++++------------- .../tests/test_getters.py | 92 ++++---- 4 files changed, 147 insertions(+), 250 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json index 3f6eb92e..4819e0bc 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json @@ -56,28 +56,76 @@ "prime_mover_types": { "BA": "battery", "BT": "geothermal", - "CA": "gas-cc", - "CC": "gas-cc", "CE": "caes", "CP": "csp", - "CS": "gas-cc", - "CT": "gas-cc", "ES": "other", "FC": "smr", "FW": "other", - "GT": "gas-ct", "HA": "hydnd", "HB": "hydnd", "HK": "hydnd", "HY": "hyded", - "IC": "lfill-gas", "PS": "pumped-hydro", "OT": "other", - "ST": "coal", "PVe": "upv", "WT": "wind-ons", "WS": "wind-ofs" }, + "reeds_thermal_mapping": { + "coal": [ + "ANTHRACITE_COAL", + "BITUMINOUS_COAL", + "SUBBITUMINOUS_COAL", + "LIGNITE_COAL", + "COAL", + "REFINED_COAL", + "WASTE_COAL", + "SYNTHESIS_GAS_COAL" + ], + "gas-cc": [ + "NATURAL_GAS" + ], + "gas-ct": [ + "OTHER_GAS", + "BLAST_FURNACE_GAS" + ], + "o-g-s": [ + "DISTILLATE_FUEL_OIL", + "RESIDUAL_FUEL_OIL", + "PETROLEUM_COKE", + "JET_FUEL", + "KEROSENE", + "PROPANE", + "WASTE_OIL", + "SYNTHESIS_GAS_PETROLEUM_COKE" + ], + "biopower": [ + "AG_BIOPRODUCT", + "AG_BYPRODUCT", + "MUNICIPAL_WASTE", + "OTHER_BIOMASS_SOLIDS", + "WOOD_WASTE_SOLIDS", + "OTHER_BIOMASS_LIQUIDS", + "SLUDGE_WASTE", + "BLACK_LIQUOR", + "WOOD_WASTE_LIQUIDS", + "TIREDERIVED_FUEL", + "WASTE_HEAT" + ], + "lfill-gas": [ + "LANDFILL_GAS", + "OTHER_BIOMASS_GAS" + ], + "nuclear": [ + "NUCLEAR" + ], + "egs": [ + "GEOTHERMAL" + ], + "other": [ + "OTHER" + ] + }, "reeds_defaults": { "syn-cond": { "capacity_MW": 320.0, diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index f2bbad2e..45d053d1 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -596,32 +596,6 @@ "target_type": "PLEXOSGenerator", "version": 1 }, - { - "defaults": { - "category": "syn-cond", - "units": 0 - }, - "field_map": { - "name": "name", - "uuid": "uuid", - "category": "category" - }, - "getters": { - "name": "get_generator_name", - "commit": "get_generator_commit", - "category": "get_generator_category", - "rating": "get_generator_rating", - "max_capacity": "get_max_capacity", - "min_stable_level": "get_generator_min_stable_level", - "forced_outage_rate": "get_generator_forced_outage_rate", - "maintenance_rate": "get_generator_maintenance_rate", - "mean_time_to_repair": "get_generator_mean_time_to_repair", - "ext": "get_component_ext" - }, - "source_type": "SynchronousCondenser", - "target_type": "PLEXOSGenerator", - "version": 1 - }, { "defaults": { "category": "head" diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 6be0287c..9c9d9e43 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -4,7 +4,6 @@ import json import math -import re # Add this near the top, after imports from collections import defaultdict @@ -84,8 +83,49 @@ def _target_system(context: PluginContext) -> Any: return cast(Any, context.target_system) +def _get_defaults_data(context: PluginContext) -> dict[str, Any]: + """Load defaults.json once per plugin context.""" + cached = context._cache.get("defaults_json") + if cached is not None: + return cast(dict[str, Any], cached) + + defaults_path = files("r2x_sienna_to_plexos.config") / "defaults.json" + with defaults_path.open() as f: + data = cast(dict[str, Any], json.load(f)) + context._cache["defaults_json"] = data + return data + + +def _get_reeds_thermal_category_from_fuel(source_component: Any, context: PluginContext) -> str | None: + """Resolve thermal ReEDS category from Sienna fuel value using defaults mapping.""" + if not isinstance(source_component, ThermalStandard | ThermalMultiStart): + return None + + fuel = getattr(source_component, "fuel", None) + if fuel is None: + return None + + fuel_str = fuel.name if hasattr(fuel, "name") else str(fuel) + fuel_key = str(fuel_str).strip().upper() + if not fuel_key: + return None + + defaults_data = _get_defaults_data(context) + mapping = defaults_data.get("reeds_thermal_mapping", {}) + if not isinstance(mapping, dict): + return None + + for category, fuel_values in mapping.items(): + if not isinstance(fuel_values, list): + continue + if fuel_key in {str(value).strip().upper() for value in fuel_values}: + return str(category) + + return None + + def _resolve_generator_category(source_component: Any, context: PluginContext) -> str | None: - """Resolve category via ext gen_type_string, ReEDS name patterns, or prime_mover mapping.""" + """Resolve category via ext gen_type_string, ReEDS name patterns, thermal fuel mapping, or prime mover.""" # Get name from ext dict ext = getattr(source_component, "ext", None) if isinstance(ext, dict): @@ -103,28 +143,18 @@ def _resolve_generator_category(source_component: Any, context: PluginContext) - if name.startswith("zonal2nodal_"): suffix = name[len("zonal2nodal_") :] - defaults_path = files("r2x_sienna_to_plexos.config") / "defaults.json" - with defaults_path.open() as f: - _z2n_defaults = json.load(f) + _z2n_defaults = _get_defaults_data(context) reeds_cats = sorted(_z2n_defaults.get("reeds_defaults", {}).keys(), key=len, reverse=True) for cat in reeds_cats: cat_str = str(cat) if suffix == cat_str or suffix.startswith(cat_str + "_"): return cat_str - # Treat explicit "nuclear" naming as high-confidence and avoid falling back to - # broad prime-mover mappings that can misclassify these units as thermal/coal. - candidate_names = [_normalize_plant_name(raw_name)] - if isinstance(ext, dict): - plant_name = ext.get("plant_name") - if plant_name: - candidate_names.append(_normalize_plant_name(str(plant_name))) - candidate_names = [c for c in dict.fromkeys(candidate_names) if c] + thermal_category = _get_reeds_thermal_category_from_fuel(source_component, context) + if thermal_category is not None: + return thermal_category - if any(_contains_nuclear_token(candidate) for candidate in candidate_names): - return "nuclear" - - # Get category from prime mover mapping when available (higher confidence than name heuristics). + # Non-thermal generators may still use prime-mover mappings. prime_mover = getattr(source_component, "prime_mover_type", None) fuel = getattr(source_component, "fuel", None) @@ -148,58 +178,15 @@ def _resolve_generator_category(source_component: Any, context: PluginContext) - if pm_only: return pm_only[0] - defaults_path = files("r2x_sienna_to_plexos.config") / "defaults.json" - with defaults_path.open() as f: - defaults_data = json.load(f) + defaults_data = _get_defaults_data(context) pm_types: dict[str, str] = defaults_data.get("prime_mover_types", {}) tech = pm_types.get(pm_str) if tech: return tech - # Name-based association for oil/nuclear is exact-match and state-aware when possible. - source_state = _normalize_state((ext or {}).get("state")) if isinstance(ext, dict) else None - - nuclear_names = _build_nuclear_plant_name_set(context) - nuclear_name_state = _build_nuclear_plant_name_state_set(context) - oil_names = _build_oil_plant_name_set(context) - oil_name_state = _build_oil_plant_name_state_set(context) - - for candidate in candidate_names: - if source_state and (candidate, source_state) in nuclear_name_state: - return "nuclear" - if source_state and (candidate, source_state) in oil_name_state: - return "oil" - - if candidate in nuclear_names: - return "nuclear" - if candidate in oil_names: - return "oil" - return None -def _normalize_plant_name(name: str) -> str: - """Normalize plant names for reliable exact matching.""" - raw = str(name) - # Split CamelCase words before punctuation cleanup (e.g., NuclearFacility -> Nuclear Facility). - raw = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", raw) - cleaned = re.sub(r"[^a-z0-9]+", " ", raw.lower()) - return " ".join(cleaned.split()) - - -def _contains_nuclear_token(name: str) -> bool: - """Return True when normalized name contains a standalone 'nuclear' token.""" - return bool(re.search(r"\bnuclear\b", name)) - - -def _normalize_state(value: Any) -> str | None: - """Normalize state to two-letter uppercase when available.""" - if value is None: - return None - state = str(value).strip().upper() - return state if state else None - - def _build_target_storage_name_index(context: PluginContext) -> dict[str, Any]: """Build PLEXOSStorage names index, cached.""" cached = context._cache.get("target_storage_name_index") @@ -289,105 +276,6 @@ def _build_battery_service_index(context: PluginContext) -> dict[str, list[Any]] return result -def _build_oil_plant_name_set(context: PluginContext) -> set[str]: - """Build normalized petroleum plant names set from us_power_plants.json, cached.""" - cached = context._cache.get("oil_plant_name_set") - if cached is not None: - return cached - plants_path = files("r2x_sienna_to_plexos.config") / "us_power_plants.json" - with plants_path.open() as f: - plants_data = json.load(f) - name_set = { - _normalize_plant_name(p["power Plant Name"]) - for p in plants_data - if isinstance(p.get("Primary Energy Source"), str) - and p["Primary Energy Source"].lower() == "petroleum" - and isinstance(p.get("power Plant Name"), str) - } - context._cache["oil_plant_name_set"] = name_set - return name_set - - -def _build_oil_plant_name_state_set(context: PluginContext) -> set[tuple[str, str]]: - """Build normalized petroleum (plant_name, state) set from us_power_plants.json, cached.""" - cached = context._cache.get("oil_plant_name_state_set") - if cached is not None: - return cached - plants_path = files("r2x_sienna_to_plexos.config") / "us_power_plants.json" - with plants_path.open() as f: - plants_data = json.load(f) - - result: set[tuple[str, str]] = set() - for plant in plants_data: - if not isinstance(plant.get("Primary Energy Source"), str): - continue - if plant["Primary Energy Source"].lower() != "petroleum": - continue - if not isinstance(plant.get("power Plant Name"), str): - continue - state = _normalize_state(plant.get("State")) - if state is None: - continue - result.add((_normalize_plant_name(plant["power Plant Name"]), state)) - - context._cache["oil_plant_name_state_set"] = result - return result - - -def _build_nuclear_plant_name_set(context: PluginContext) -> set[str]: - """Build normalized nuclear plant names set from defaults.json and us_power_plants.json, cached.""" - cached = context._cache.get("nuclear_plant_name_set") - if cached is not None: - return cached - - # From defaults.json nuclear_plants list - defaults_path = files("r2x_sienna_to_plexos.config") / "defaults.json" - with defaults_path.open() as f: - defaults_data = json.load(f) - name_set = {_normalize_plant_name(p["name"]) for p in defaults_data.get("nuclear_plants", [])} - - # From us_power_plants.json filtered by Primary Energy Source == "nuclear" - plants_path = files("r2x_sienna_to_plexos.config") / "us_power_plants.json" - with plants_path.open() as f: - plants_data = json.load(f) - name_set |= { - _normalize_plant_name(p["power Plant Name"]) - for p in plants_data - if isinstance(p.get("Primary Energy Source"), str) - and p["Primary Energy Source"].lower() == "nuclear" - and isinstance(p.get("power Plant Name"), str) - } - - context._cache["nuclear_plant_name_set"] = name_set - return name_set - - -def _build_nuclear_plant_name_state_set(context: PluginContext) -> set[tuple[str, str]]: - """Build normalized nuclear (plant_name, state) set from us_power_plants.json, cached.""" - cached = context._cache.get("nuclear_plant_name_state_set") - if cached is not None: - return cached - plants_path = files("r2x_sienna_to_plexos.config") / "us_power_plants.json" - with plants_path.open() as f: - plants_data = json.load(f) - - result: set[tuple[str, str]] = set() - for plant in plants_data: - if not isinstance(plant.get("Primary Energy Source"), str): - continue - if plant["Primary Energy Source"].lower() != "nuclear": - continue - if not isinstance(plant.get("power Plant Name"), str): - continue - state = _normalize_state(plant.get("State")) - if state is None: - continue - result.add((_normalize_plant_name(plant["power Plant Name"]), state)) - - context._cache["nuclear_plant_name_state_set"] = result - return result - - def _build_area_buses_index(context: PluginContext) -> dict[str, list[Any]]: """Map area_name -> list of ACBus components in that area.""" cached = context._cache.get("area_buses_index") @@ -1429,14 +1317,15 @@ def get_3w_transformer_tertiary_rating( @getter def get_generator_category(source_component: object, context: PluginContext) -> Result[str, ValueError]: - """Determine generator category using ReEDS tech names, gen_type_string, or prime_mover/fuel mapping. + """Determine generator category using ReEDS tech names, gen_type_string, and fuel/prime-mover mapping. Priority: 1. ext["gen_type_string"] mapped through _GEN_TYPE_STRING_MAP 2. ReEDS component name patterns (hydend, hyded, distpv, wind-ofs, etc.) - 3. prime_mover + fuel via context.config.prime_mover_mapping - 4. prime_mover abbreviation via defaults.json prime_mover_types - 5. Err → rule default applies + 3. ThermalStandard/ThermalMultiStart fuel via defaults.json reeds_thermal_mapping + 4. prime_mover + fuel via context.config.prime_mover_mapping (non-thermal fallback) + 5. prime_mover abbreviation via defaults.json prime_mover_types (non-thermal fallback) + 6. Err -> rule default applies """ category = _resolve_generator_category(source_component, context) if category is not None: diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index f88a4b35..e865d9e7 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -644,70 +644,56 @@ class DummyThermal: assert getters.get_thermal_generator_units(DummyThermal(), context).unwrap() == 1 -def test_get_generator_category_avoids_substring_nuclear_false_positive(monkeypatch, context): - class DummyGenerator: - name = "wind_plant_component" - ext = {"plant_name": "Monticello Wind Farm"} # noqa: RUF012 - prime_mover_type = None - fuel = None - - monkeypatch.setattr(getters, "_build_nuclear_plant_name_set", lambda _ctx: {"monticello"}) - monkeypatch.setattr(getters, "_build_nuclear_plant_name_state_set", lambda _ctx: set()) - monkeypatch.setattr(getters, "_build_oil_plant_name_set", lambda _ctx: set()) - monkeypatch.setattr(getters, "_build_oil_plant_name_state_set", lambda _ctx: set()) - - assert getters.get_generator_category(DummyGenerator(), context).is_err() - +def _make_thermal_generator_for_category_tests( + name: str, + fuel: ThermalFuels | str, + prime_mover_type: PrimeMoversType = PrimeMoversType.CC, +) -> ThermalStandard: + return ThermalStandard( + name=name, + bus=None, + active_power=0.0, + reactive_power=0.0, + rating=100.0, + base_power=10.0, + must_run=False, + status=True, + time_at_status=0.0, + active_power_limits=MinMax(min=10.0, max=100.0), + ramp_limits=UpDown(up=10.0, down=10.0), + time_limits=UpDown(up=1.0, down=1.0), + prime_mover_type=prime_mover_type, + fuel=fuel, + operation_cost=ThermalGenerationCost.example(), + ) -def test_get_generator_category_uses_state_for_nuclear_matching(monkeypatch, context): - class DummyGenerator: - name = "gen" - ext = {"plant_name": "Monticello", "state": "TX"} # noqa: RUF012 - prime_mover_type = None - fuel = None - monkeypatch.setattr(getters, "_build_nuclear_plant_name_set", lambda _ctx: set()) - monkeypatch.setattr( - getters, - "_build_nuclear_plant_name_state_set", - lambda _ctx: {("monticello", "MN")}, +def test_get_generator_category_maps_thermal_nuclear_fuel(context): + gen = _make_thermal_generator_for_category_tests( + name="thermal-nuclear", + fuel=ThermalFuels.NUCLEAR, ) - monkeypatch.setattr(getters, "_build_oil_plant_name_set", lambda _ctx: set()) - monkeypatch.setattr(getters, "_build_oil_plant_name_state_set", lambda _ctx: set()) - assert getters.get_generator_category(DummyGenerator(), context).is_err() + assert getters.get_generator_category(gen, context).unwrap() == "nuclear" -def test_get_generator_category_matches_nuclear_with_same_state(monkeypatch, context): - class DummyGenerator: - name = "gen" - ext: ClassVar[dict[str, str]] = {"plant_name": "Monticello", "state": "MN"} - prime_mover_type = None - fuel = None - - monkeypatch.setattr(getters, "_build_nuclear_plant_name_set", lambda _ctx: set()) - monkeypatch.setattr( - getters, - "_build_nuclear_plant_name_state_set", - lambda _ctx: {("monticello", "MN")}, +def test_get_generator_category_maps_thermal_oil_fuel(context): + gen = _make_thermal_generator_for_category_tests( + name="thermal-oil", + fuel=ThermalFuels.KEROSENE, ) - monkeypatch.setattr(getters, "_build_oil_plant_name_set", lambda _ctx: set()) - monkeypatch.setattr(getters, "_build_oil_plant_name_state_set", lambda _ctx: set()) - assert getters.get_generator_category(DummyGenerator(), context).unwrap() == "nuclear" + assert getters.get_generator_category(gen, context).unwrap() == "o-g-s" -def test_get_generator_category_prioritizes_nuclear_keyword_over_prime_mover(context): - class DummyPrimeMover: - name = "ST" - - class DummyGenerator: - name = "MonticelliNuclearFacility_1" - ext = {} # noqa: RUF012 - prime_mover_type = DummyPrimeMover() - fuel = None +def test_get_generator_category_thermal_prefers_fuel_over_prime_mover(context): + gen = _make_thermal_generator_for_category_tests( + name="thermal-gas", + fuel=ThermalFuels.NATURAL_GAS, + prime_mover_type=PrimeMoversType.ST, + ) - assert getters.get_generator_category(DummyGenerator(), context).unwrap() == "nuclear" + assert getters.get_generator_category(gen, context).unwrap() == "gas-cc" def test_get_turbine_pump_load_and_efficiency(context): From 325297357323ac6ce0c003f3e6c5c58bcda364e2 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 09:12:54 -0600 Subject: [PATCH 05/16] fix: remove line/trf properties not used in the pcm --- .../r2x_sienna_to_plexos/config/rules.json | 7 ++-- .../src/r2x_sienna_to_plexos/getters.py | 36 ------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index 45d053d1..af4157c9 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -12,7 +12,6 @@ "units": "get_availability", "voltage": "get_voltage_kv", "is_slack_bus": "is_slack_bus", - "ac_voltage_magnitude": "get_ac_voltage_magnitude_pu", "load_participation_factor": "get_load_participation_factor" }, "source_type": "ACBus", @@ -57,8 +56,7 @@ "max_flow": "get_line_max_flow", "loss_incr": "lines_loss_incremental", "wheeling_charge": "lines_wheeling_charge", - "wheeling_charge_back": "lines_wheeling_charge_back", - "ac_line_charging_susceptance": "get_line_charging_susceptance" + "wheeling_charge_back": "lines_wheeling_charge_back" }, "source_type": "Line", "target_type": "PLEXOSLine", @@ -81,8 +79,7 @@ "max_flow": "get_line_max_flow", "loss_incr": "lines_loss_incremental", "wheeling_charge": "lines_wheeling_charge", - "wheeling_charge_back": "lines_wheeling_charge_back", - "ac_line_charging_susceptance": "get_line_charging_susceptance" + "wheeling_charge_back": "lines_wheeling_charge_back" }, "source_type": "MonitoredLine", "target_type": "PLEXOSLine", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 9c9d9e43..7d562e07 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -54,7 +54,6 @@ from r2x_sienna.models.getters import ( get_max_active_power as sienna_get_max_active_power, ) -from r2x_sienna.models.named_tuples import FromTo_ToFrom from r2x_sienna.units import get_magnitude from r2x_core import Err, Ok, PluginContext, Result @@ -870,13 +869,6 @@ def get_voltage_kv(source_component: ACBus, context: PluginContext) -> Result[fl return Ok(round(float(value), 1) if value is not None else 0.0) -@getter -def get_ac_voltage_magnitude_pu(source_component: ACBus, context: PluginContext) -> Result[float, ValueError]: - """Extract AC voltage magnitude in per unit from the source component.""" - value = getattr(source_component, "magnitude", None) - return Ok(round(float(value), 3) if value is not None else 1.0) - - @getter def get_node_category(source_component: ACBus, context: PluginContext) -> Result[str, ValueError]: """Return the Area name for the bus, since Area maps to PLEXOSRegion.""" @@ -1117,34 +1109,6 @@ def lines_wheeling_charge_back( return Ok(float(wc_back)) -@getter -def get_line_charging_susceptance( - source_component: Line | MonitoredLine, context: PluginContext -) -> Result[float, ValueError]: - """Extract line charging susceptance as float from source component.""" - match getattr(source_component, "b", None): - case None: - return Ok(0.0) - case int() | float() as val: - return Ok(float(val)) - case complex() as val: - return Ok(float(val.imag)) - case FromTo_ToFrom() as val: - return Ok(float(val.from_to)) - case dict() as val: - match val.get("from_to"): - case int() | float() as ft: - return Ok(float(ft)) - case _: - return Ok(0.0) - case val: - match get_magnitude(val): - case int() | float() as mag: - return Ok(float(mag)) - case _: - return Ok(0.0) - - @getter def get_vsc_line_resistance( source_component: TwoTerminalVSCLine, context: PluginContext From c9f8b26d21780bfe413e5a645b522ee6dfd1d7dd Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 09:31:59 -0600 Subject: [PATCH 06/16] feat: add reference node logic for region-node membership --- .../src/r2x_sienna_to_plexos/__init__.py | 2 + .../src/r2x_sienna_to_plexos/getters_utils.py | 34 ++++ .../src/r2x_sienna_to_plexos/translation.py | 2 + .../tests/test_getters_utils.py | 56 +++++- .../test_translation_rule_application.py | 2 + uv.lock | 188 +++++++++++++++--- 6 files changed, 250 insertions(+), 34 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py index 9e4172e9..5c36ead8 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py @@ -26,6 +26,7 @@ ensure_generator_node_memberships, ensure_head_storage_generator_membership, ensure_interface_line_memberships, + ensure_reference_node_memberships, ensure_region_node_memberships, ensure_reserve_battery_memberships, ensure_reserve_generator_memberships, @@ -50,6 +51,7 @@ "sienna_to_plexos", "REEDS_COMPONENT_SUBSTRINGS", "ensure_region_node_memberships", + "ensure_reference_node_memberships", "ensure_interface_line_memberships", "ensure_generator_node_memberships", "ensure_battery_node_memberships", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index 74f52c0e..e8a0df7e 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -36,6 +36,7 @@ TransmissionInterface, VariableReserve, ) +from r2x_sienna.models.enums import ACBusTypes from r2x_sienna.units import get_magnitude if TYPE_CHECKING: @@ -231,6 +232,39 @@ def ensure_region_node_memberships(context: PluginContext) -> None: logger.info("Total {} Region-Node memberships created.", total_memberships) +def ensure_reference_node_memberships(context: PluginContext) -> None: + """Create Region->Node memberships in ReferenceNode for REF/SLACK bus regions only.""" + bus_index = _bus_name_to_area_and_zone(context) + regions_by_name = {r.name: r for r in _target_system(context).get_components(PLEXOSRegion)} + + ref_region_names: set[str] = set() + for bus in _source_system(context).get_components(ACBus): + bustype = getattr(bus, "bustype", None) + bustype_name = getattr(bustype, "name", str(bustype)).upper() if bustype is not None else "" + if bustype not in {ACBusTypes.REF, ACBusTypes.SLACK} and bustype_name not in {"REF", "SLACK"}: + continue + + area_name, _ = bus_index.get(bus.name, (None, None)) + if area_name is not None: + ref_region_names.add(area_name) + + if not ref_region_names: + logger.info("No REF/SLACK buses found. Skipping ReferenceNode memberships.") + return + + total_memberships = 0 + for node in _target_system(context).get_components(PLEXOSNode): + area_name, _ = bus_index.get(node.name, (None, None)) + if area_name is None or area_name not in ref_region_names: + continue + region = regions_by_name.get(area_name) + if region is not None: + _ensure_membership(context, region, node, CollectionEnum.ReferenceNode) + total_memberships += 1 + + logger.info("Total {} ReferenceNode Region->Node memberships created.", total_memberships) + + def _extract_base_name(name: str) -> str: for suffix in ("_Turbine", "_Reservoir_head", "_Reservoir_tail", "_Reservoir", "_head", "_tail"): if name.endswith(suffix): diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py index 2d326e9c..7a857131 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py @@ -17,6 +17,7 @@ ensure_generator_time_series, ensure_head_storage_generator_membership, ensure_interface_line_memberships, + ensure_reference_node_memberships, ensure_region_node_memberships, ensure_reserve_battery_memberships, ensure_reserve_generator_memberships, @@ -59,6 +60,7 @@ def sienna_to_plexos(system: System, config: SiennaToPlexosConfig) -> System: ensure_generator_time_series(context) ensure_reserve_time_series(context) ensure_region_node_memberships(context) + ensure_reference_node_memberships(context) ensure_generator_node_memberships(context) ensure_battery_node_memberships(context) ensure_reserve_battery_memberships(context) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index 5ece23cf..cd4f36ef 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -31,7 +31,7 @@ VariableReserve, ) from r2x_sienna.models.costs import ThermalGenerationCost -from r2x_sienna.models.enums import PrimeMoversType, ReserveType, StorageTechs, ThermalFuels +from r2x_sienna.models.enums import ACBusTypes, PrimeMoversType, ReserveType, StorageTechs, ThermalFuels from r2x_sienna.models.named_tuples import Complex, InputOutput, MinMax, UpDown from r2x_sienna_to_plexos import getters_utils @@ -211,6 +211,60 @@ def test_ensure_transformer_node_memberships(context): assert any(m.collection in (CollectionEnum.NodeFrom, CollectionEnum.NodeTo) for m in memberships) +def test_ensure_reference_node_memberships_only_for_ref_region(context): + area_ref = Area(name="A1") + area_other = Area(name="A2") + region_ref = PLEXOSRegion(name="A1") + region_other = PLEXOSRegion(name="A2") + node_ref = PLEXOSNode(name="N1") + node_other = PLEXOSNode(name="N2") + bus_ref = ACBus(name="N1", number=1, bustype=ACBusTypes.REF, area=area_ref) + bus_other = ACBus(name="N2", number=2, bustype=ACBusTypes.PQ, area=area_other) + + context.source_system.add_component(area_ref) + context.source_system.add_component(area_other) + context.target_system.add_component(region_ref) + context.target_system.add_component(region_other) + context.target_system.add_component(node_ref) + context.target_system.add_component(node_other) + context.source_system.add_component(bus_ref) + context.source_system.add_component(bus_other) + + getters_utils.ensure_reference_node_memberships(context) + + ref_memberships = context.target_system.get_supplemental_attributes_with_component( + node_ref, PLEXOSMembership + ) + other_memberships = context.target_system.get_supplemental_attributes_with_component( + node_other, PLEXOSMembership + ) + + assert any( + m.collection == CollectionEnum.ReferenceNode + and m.parent_object == region_ref + and m.child_object == node_ref + for m in ref_memberships + ) + assert not any(m.collection == CollectionEnum.ReferenceNode for m in other_memberships) + + +def test_ensure_reference_node_memberships_skips_when_no_ref_bus(context): + area = Area(name="A1") + region = PLEXOSRegion(name="A1") + node = PLEXOSNode(name="N1") + bus = ACBus(name="N1", number=1, bustype=ACBusTypes.PQ, area=area) + + context.source_system.add_component(area) + context.target_system.add_component(region) + context.target_system.add_component(node) + context.source_system.add_component(bus) + + getters_utils.ensure_reference_node_memberships(context) + + memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) + assert not any(m.collection == CollectionEnum.ReferenceNode for m in memberships) + + def test_ensure_head_tail_storage_generator_membership(context): gen = PLEXOSGenerator(name="foo_head") storage = PLEXOSStorage(name="foo_head") diff --git a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py index 8519f6d9..3398583a 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py +++ b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py @@ -306,6 +306,7 @@ def _inner(_context): monkeypatch.setattr(translation_module, "ensure_generator_time_series", _mark("gen_ts")) monkeypatch.setattr(translation_module, "ensure_reserve_time_series", _mark("reserve_ts")) monkeypatch.setattr(translation_module, "ensure_region_node_memberships", _mark("region_node")) + monkeypatch.setattr(translation_module, "ensure_reference_node_memberships", _mark("reference_node")) monkeypatch.setattr(translation_module, "ensure_generator_node_memberships", _mark("gen_node")) monkeypatch.setattr(translation_module, "ensure_battery_node_memberships", _mark("battery_node")) monkeypatch.setattr(translation_module, "ensure_reserve_battery_memberships", _mark("reserve_battery")) @@ -324,6 +325,7 @@ def _inner(_context): "gen_ts", "reserve_ts", "region_node", + "reference_node", "gen_node", "battery_node", "reserve_battery", diff --git a/uv.lock b/uv.lock index acfcb427..01d3e0f8 100644 --- a/uv.lock +++ b/uv.lock @@ -817,14 +817,37 @@ wheels = [ [[package]] name = "plexosdb" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } +version = "1.3.4" +source = { editable = "../plexosdb" } dependencies = [ { name = "loguru" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/43/fa05e53424133cd01cdb09ab9f823138c6aecf8b839f3ddabffb78ac5cc2/plexosdb-1.3.0.tar.gz", hash = "sha256:71ecbcf4c505d8a446ea7fbcc14fe077f20a2ceb874ab9156d9ac74a8d4c3550", size = 48882, upload-time = "2025-12-11T18:38:37.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e5/190634b0b9a65ac3a951004bb24b1e60ef546d31961f27bc16e086dc76d1/plexosdb-1.3.0-py3-none-any.whl", hash = "sha256:b527b6476530262ca81f35032bac809de145ad458c4d7f2d5a8b7c1629f7be6e", size = 52771, upload-time = "2025-12-11T18:38:35.777Z" }, + +[package.metadata] +requires-dist = [{ name = "loguru" }] + +[package.metadata.requires-dev] +dev = [ + { name = "ipython", specifier = ">=9.2.0" }, + { name = "prek", specifier = ">=0.3.3" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-benchmark", specifier = ">=5.1.0" }, + { name = "pytest-coverage", specifier = ">=0.0" }, + { name = "ruff", specifier = ">=0.11.5" }, + { name = "ty", specifier = ">=0.0.23" }, +] +docs = [ + { name = "docstr-coverage", specifier = ">=2.3.2" }, + { name = "furo" }, + { name = "ghp-import" }, + { name = "myst-parser" }, + { name = "sphinx-autobuild", specifier = ">=2024.10.3" }, + { name = "sphinx-book-theme" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-multiversion", specifier = ">=0.2.4" }, + { name = "sphinx-tabs" }, + { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-versioning", specifier = ">=2.2.1" }, ] [[package]] @@ -1123,9 +1146,14 @@ name = "r2x" version = "2.0.0" source = { editable = "." } dependencies = [ + { name = "plexosdb" }, + { name = "r2x-core" }, + { name = "r2x-plexos" }, { name = "r2x-plexos-to-sienna" }, + { name = "r2x-reeds" }, { name = "r2x-reeds-to-plexos" }, { name = "r2x-reeds-to-sienna" }, + { name = "r2x-sienna" }, { name = "r2x-sienna-to-plexos" }, ] @@ -1152,9 +1180,14 @@ docs = [ [package.metadata] requires-dist = [ + { name = "plexosdb", editable = "../plexosdb" }, + { name = "r2x-core", editable = "../r2x-core" }, + { name = "r2x-plexos", editable = "../r2x-plexos" }, { name = "r2x-plexos-to-sienna", editable = "packages/r2x-plexos-to-sienna" }, + { name = "r2x-reeds", editable = "../r2x-reeds" }, { name = "r2x-reeds-to-plexos", editable = "packages/r2x-reeds-to-plexos" }, { name = "r2x-reeds-to-sienna", editable = "packages/r2x-reeds-to-sienna" }, + { name = "r2x-sienna", editable = "../r2x-sienna" }, { name = "r2x-sienna-to-plexos", editable = "packages/r2x-sienna-to-plexos" }, ] @@ -1166,7 +1199,7 @@ dev = [ { name = "prek", specifier = ">=0.3.8" }, { name = "pytest", specifier = ">=8.3.0,<9.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0,<6.0.0" }, - { name = "r2x-core", git = "https://github.com/NREL/r2x-core.git?branch=main" }, + { name = "r2x-core", editable = "../r2x-core" }, { name = "ruff", specifier = ">=0.6.8,<0.7.0" }, { name = "taplo", specifier = ">=0.9.3,<1.0.0" }, { name = "ty", specifier = ">=0.0.2" }, @@ -1182,7 +1215,7 @@ docs = [ [[package]] name = "r2x-core" version = "0.4.2" -source = { git = "https://github.com/NREL/r2x-core.git?branch=main#9c8f8d1e088bd3e6077d9255ca2f884b53b0dd74" } +source = { editable = "../r2x-core" } dependencies = [ { name = "h5py" }, { name = "infrasys" }, @@ -1193,22 +1226,81 @@ dependencies = [ { name = "rust-ok" }, ] +[package.metadata] +requires-dist = [ + { name = "h5py", specifier = ">=3.13.0,<4.0.0" }, + { name = "infrasys", specifier = ">=1.0.0,<2.0.0" }, + { name = "loguru", specifier = ">=0.7.3,<0.8.0" }, + { name = "packaging", specifier = ">=24.0,<26.0" }, + { name = "polars", specifier = ">=1.33.1,<2.0.0" }, + { name = "pydantic", specifier = ">=2.11.9,<3.0.0" }, + { name = "rust-ok", specifier = ">=0.3.0,<1.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "h5py-stubs", specifier = ">=0.1.2,<1.0.0" }, + { name = "mypy", specifier = ">=1.18.2,<2.0.0" }, + { name = "prek", specifier = ">=0.2.29,<0.3.0" }, + { name = "pytest", specifier = ">=8.4.2,<10.0.0" }, + { name = "pytest-coverage", specifier = ">=0.0,<1.0.0" }, + { name = "ruff", specifier = ">=0.13.2,<0.15.0" }, + { name = "ty", specifier = ">=0.0.12,<1.0.0" }, +] +docs = [ + { name = "autodoc-pydantic", specifier = ">=2.0.0,<3.0.0" }, + { name = "docstr-coverage", specifier = ">=2.3.2,<3.0.0" }, + { name = "furo", specifier = ">=2025.9.25,<2026.0.0" }, + { name = "ghp-import", specifier = ">=2.1.0,<3.0.0" }, + { name = "myst-parser", specifier = ">=3.0.0,<5.0.0" }, + { name = "sphinx", specifier = ">=7.2.0,<8.0.0" }, + { name = "sphinx-copybutton", specifier = ">=0.5.2,<1.0.0" }, + { name = "sphinx-reports", specifier = ">=0.1.0,<1.0.0" }, + { name = "sphinx-tabs", specifier = ">=3.4.5,<4.0.0" }, + { name = "sphinxcontrib-mermaid", specifier = ">=2.0.0,<3.0.0" }, +] + [[package]] name = "r2x-plexos" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } +version = "0.1.5" +source = { editable = "../r2x-plexos" } dependencies = [ { name = "plexosdb" }, { name = "r2x-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/43/99172cfe9aaea9336843c1d0a5b5e2c17d5b44fbb14756547d4be0aed563/r2x_plexos-0.1.4.tar.gz", hash = "sha256:37df059b0c9d9fecd0b68078cfcb38e5fdd3d1350f7f364a8bf0955f329abfa5", size = 815201, upload-time = "2026-04-07T01:48:50.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/25/5268e5989754bd43860610dc78ec6c378325a28a779d04ecb2937e9ea696/r2x_plexos-0.1.4-py3-none-any.whl", hash = "sha256:405640c16b7037e2c97693062e10cbe2cfa94a8df9f4a50d72efc38f976bb273", size = 832160, upload-time = "2026-04-07T01:48:56.314Z" }, + +[package.metadata] +requires-dist = [ + { name = "plexosdb", specifier = ">=1.3.0" }, + { name = "r2x-core", specifier = ">=0.4.0,<1.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ipython", specifier = ">=9.2.0" }, + { name = "mypy", specifier = ">=1.15.0" }, + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "prek", specifier = ">=0.2.0,<0.3.0" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-coverage", specifier = ">=0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.11.5" }, +] +docs = [ + { name = "autodoc-pydantic" }, + { name = "docstr-coverage", specifier = ">=2.3.2" }, + { name = "furo", specifier = ">=2025.9.25" }, + { name = "ghp-import" }, + { name = "myst-parser" }, + { name = "sphinx", specifier = ">=8.0.0" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-reports" }, + { name = "sphinx-tabs" }, ] [[package]] name = "r2x-plexos-to-sienna" -version = "0.0.0" +version = "0.1.0" source = { editable = "packages/r2x-plexos-to-sienna" } dependencies = [ { name = "r2x-plexos" }, @@ -1217,25 +1309,46 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "r2x-plexos", specifier = ">=0.1.4,<1.0.0" }, - { name = "r2x-sienna", specifier = ">=0.2.0,<1.0.0" }, + { name = "r2x-plexos", editable = "../r2x-plexos" }, + { name = "r2x-sienna", editable = "../r2x-sienna" }, ] [[package]] name = "r2x-reeds" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } +version = "0.5.0" +source = { editable = "../r2x-reeds" } dependencies = [ { name = "r2x-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/bd/1c0bbc0ef14380d56efac5d5a81628cbed050dc6d9a05262bbeb536ef7d8/r2x_reeds-0.4.0.tar.gz", hash = "sha256:98078cc7b35f8941ea9066305bf24a0cc6c8e4b2969bf5d9d09cb159bfa006b3", size = 59692, upload-time = "2026-03-18T15:16:29.607Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/07/b932e6ba9fca1d2d9ebc3dc95df88957ac7eaafe6af2330313c871900d28/r2x_reeds-0.4.0-py3-none-any.whl", hash = "sha256:2d0b0de53f3ec783e6d1321a7ad6228feebae05d8263d645430ee0fcf719898d", size = 73340, upload-time = "2026-03-18T15:16:28.173Z" }, + +[package.metadata] +requires-dist = [{ name = "r2x-core", specifier = ">=0.4.1,<1.0.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "prek", specifier = ">=0.3.0,<1.0.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-coverage", specifier = ">=0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.13.3" }, + { name = "ty", specifier = ">=0.0.14" }, +] +docs = [ + { name = "autodoc-pydantic" }, + { name = "docstr-coverage", specifier = ">=2.3.2" }, + { name = "furo", specifier = ">=2025.9.25" }, + { name = "ghp-import" }, + { name = "myst-parser" }, + { name = "sphinx", specifier = ">=8.0.0" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-reports" }, + { name = "sphinx-tabs" }, ] [[package]] name = "r2x-reeds-to-plexos" -version = "0.0.0" +version = "0.1.0" source = { editable = "packages/r2x-reeds-to-plexos" } dependencies = [ { name = "r2x-plexos" }, @@ -1244,13 +1357,13 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "r2x-plexos", specifier = ">=0.1.4,<1.0.0" }, - { name = "r2x-reeds", specifier = ">=0.4.0,<1.0.0" }, + { name = "r2x-plexos", editable = "../r2x-plexos" }, + { name = "r2x-reeds", editable = "../r2x-reeds" }, ] [[package]] name = "r2x-reeds-to-sienna" -version = "0.0.0" +version = "0.1.0" source = { editable = "packages/r2x-reeds-to-sienna" } dependencies = [ { name = "r2x-reeds" }, @@ -1259,25 +1372,34 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "r2x-reeds", specifier = ">=0.4.0,<1.0.0" }, - { name = "r2x-sienna", specifier = ">=0.2.0,<1.0.0" }, + { name = "r2x-reeds", editable = "../r2x-reeds" }, + { name = "r2x-sienna", editable = "../r2x-sienna" }, ] [[package]] name = "r2x-sienna" version = "0.2.1" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../r2x-sienna" } dependencies = [ { name = "r2x-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/51/641a6a8f40c126c5fd989cb94f47bc0a53c0c5f72c69a6f96f757914efca/r2x_sienna-0.2.1.tar.gz", hash = "sha256:7555881adc4f8964f87aac2f4799aaa8db56df6f294d3a9fa0e43d90dffe0bc6", size = 53073, upload-time = "2026-04-06T21:10:17.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f1/0b734a4fb36480c8d56a144c859c6f003ac0c2712ff29f5f1abeeae7a828/r2x_sienna-0.2.1-py3-none-any.whl", hash = "sha256:231ccd52908eac2b3a084659215c6cb61689a168ba7257e45e94e6038d60c26d", size = 62203, upload-time = "2026-04-06T21:10:15.833Z" }, + +[package.metadata] +requires-dist = [{ name = "r2x-core", specifier = ">=0.4.0,<1.0.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "ipython", specifier = ">=8.32.0" }, + { name = "prek", specifier = ">=0.2.28" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-coverage", specifier = ">=0.0" }, + { name = "ruff", specifier = ">=0.9.3" }, + { name = "ty", specifier = ">=0.0.2" }, ] [[package]] name = "r2x-sienna-to-plexos" -version = "0.0.0" +version = "0.1.0" source = { editable = "packages/r2x-sienna-to-plexos" } dependencies = [ { name = "r2x-plexos" }, @@ -1286,8 +1408,8 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "r2x-plexos", specifier = ">=0.1.4,<1.0.0" }, - { name = "r2x-sienna", specifier = ">=0.2.0,<1.0.0" }, + { name = "r2x-plexos", editable = "../r2x-plexos" }, + { name = "r2x-sienna", editable = "../r2x-sienna" }, ] [[package]] From f7c91dc66e328575fa29398846f5847668f62951 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 15 Apr 2026 15:17:12 -0600 Subject: [PATCH 07/16] fix: update max capacity further to be equal to rating in default cases --- .../src/r2x_sienna_to_plexos/getters.py | 29 +++------------- .../tests/test_getters.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 7d562e07..7f90d691 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -1357,41 +1357,20 @@ def get_thermal_generator_units( def get_max_capacity(source_component: object, context: PluginContext) -> Result[float, ValueError]: """Extract maximum capacity in MW from rating, active_power_limits, or max_active_power. - If extracted capacity is below 10 MW, replace it with the category-level - ``max_capacity_MW`` default. + When rating is available, max_capacity must match rating exactly. """ - def _apply_small_capacity_default(capacity_mw: float) -> float: - """Replace tiny capacities with category default max capacity.""" - if capacity_mw >= 10.0: - return round(capacity_mw, 2) - - category = _resolve_generator_category(source_component, context) or "gas-cc" - default_max = _get_defaults(category, "max_capacity_MW") - - if math.isclose(default_max, 0.0, rel_tol=0.0, abs_tol=1e-9): - # Backstop for categories that may not define max_capacity_MW. - default_max = _get_defaults(category, "capacity_MW") - - if default_max > 0.0: - return round(default_max, 2) - generic_default = _get_defaults("gas-cc", "max_capacity_MW") or _get_defaults("gas-cc", "capacity_MW") - if generic_default > 0.0: - return round(generic_default, 2) - - return round(capacity_mw, 2) - rating = getattr(source_component, "rating", None) rating_value = get_magnitude(rating) if rating_value is not None: capacity = abs(float(rating_value) * resolve_base_power(source_component)) - return Ok(_apply_small_capacity_default(capacity)) + return Ok(round(capacity, 2)) limits = getattr(source_component, "active_power_limits", None) if isinstance(limits, dict): max_value = limits.get("max") if isinstance(max_value, int | float): - return Ok(_apply_small_capacity_default(abs(float(max_value)))) + return Ok(round(abs(float(max_value)), 2)) try: value = sienna_get_max_active_power(source_component) @@ -1399,7 +1378,7 @@ def _apply_small_capacity_default(capacity_mw: float) -> float: value = None if value is not None: - return Ok(_apply_small_capacity_default(abs(float(value)))) + return Ok(round(abs(float(value)), 2)) return Err(ValueError("active_power_limits or rating missing")) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index e865d9e7..16d11a8b 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -312,6 +312,40 @@ def test_get_max_capacity_with_limits(context): assert getters.get_max_capacity(gen, context).unwrap() == 1000.0 +def test_get_max_capacity_matches_rating_for_small_values(context): + gen = ThermalStandard( + name="GEN_SMALL", + bus=ACBus(name="N1", base_voltage=115.0, number=1), + active_power=0.0, + reactive_power=0.0, + rating=7.1, + base_power=1.0, + must_run=False, + status=True, + time_at_status=0.0, + active_power_limits=MinMax(min=0.0, max=999.0), + ramp_limits=UpDown(up=10.0, down=10.0), + time_limits=UpDown(up=1.0, down=1.0), + prime_mover_type=PrimeMoversType.CC, + fuel=ThermalFuels.NATURAL_GAS, + operation_cost=ThermalGenerationCost( + variable=FuelCurve( + value_curve=InputOutputCurve( + function_data=QuadraticFunctionData( + quadratic_term=0.01, + proportional_term=9.0, + constant_term=100.0, + ) + ), + fuel_cost=2.0, + power_units=UnitSystem.NATURAL_UNITS, + ), + ), + ) + assert getters.get_generator_rating(gen, context).unwrap() == 7.1 + assert getters.get_max_capacity(gen, context).unwrap() == 7.1 + + def test_get_storage_charge_discharge_efficiency_valid(context): battery = EnergyReservoirStorage( name="BAT1", From f915a73a93d8f42e31fbe8256f487752ba87bfc1 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 16 Apr 2026 14:36:16 -0600 Subject: [PATCH 08/16] fix: update storage memberships to exist only when pumped-hydro gens are enabled for it --- .../src/r2x_sienna_to_plexos/getters.py | 19 +++++ .../src/r2x_sienna_to_plexos/getters_utils.py | 33 ++++++++ .../tests/test_getters.py | 4 +- .../tests/test_getters_utils.py | 75 +++++++++++++++++-- 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 7f90d691..f4c948bd 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -28,6 +28,7 @@ DiscreteControlledACBranch, EnergyReservoirStorage, HydroDispatch, + HydroPumpedStorage, HydroPumpTurbine, HydroReservoir, HydroTurbine, @@ -2357,11 +2358,23 @@ def _build_reservoir_by_turbine_index(context: PluginContext) -> dict[str, Any]: return index +def _is_hydro_pumped_storage_generator(context: PluginContext, gen_name: str) -> bool: + """Return True when target generator name resolves to a source HydroPumpedStorage.""" + source_generator = _lookup_source_generator(context, gen_name) + return isinstance(source_generator, HydroPumpedStorage) + + @getter def membership_head_storage_generator( generator: HydroTurbine, context: PluginContext ) -> Result[Any, ValueError]: gen_name = getattr(generator, "name", "") + if not _is_hydro_pumped_storage_generator(context, gen_name): + return Err( + ValueError( + f"Skipping HeadStorage membership for '{gen_name}': source generator is not HydroPumpedStorage" + ) + ) storage_index = _build_target_storage_name_index(context) # Primary: look up which reservoir owns this turbine @@ -2398,6 +2411,12 @@ def membership_tail_storage_generator( generator: HydroTurbine, context: PluginContext ) -> Result[Any, ValueError]: gen_name = getattr(generator, "name", "") + if not _is_hydro_pumped_storage_generator(context, gen_name): + return Err( + ValueError( + f"Skipping TailStorage membership for '{gen_name}': source generator is not HydroPumpedStorage" + ) + ) storage_index = _build_target_storage_name_index(context) # Primary: look up which reservoir owns this turbine diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index e8a0df7e..808a53c2 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -27,6 +27,7 @@ ACBus, Area, EnergyReservoirStorage, + HydroPumpedStorage, HydroReservoir, HydroTurbine, LoadZone, @@ -272,6 +273,24 @@ def _extract_base_name(name: str) -> str: return name +def _build_pumped_storage_target_generator_name_index(context: PluginContext) -> set[str]: + """Return target generator names that originate from Sienna HydroPumpedStorage.""" + cache_key = "pumped_storage_target_generator_name_index" + cached = context._cache.get(cache_key) + if cached is not None: + return cast(set[str], cached) + + from r2x_sienna_to_plexos.getters import _build_generator_display_name_index + + display_name_index = _build_generator_display_name_index(context) + allowed_names = { + display_name_index.get(source_gen.name, source_gen.name) + for source_gen in _source_system(context).get_components(HydroPumpedStorage) + } + context._cache[cache_key] = allowed_names + return allowed_names + + def ensure_head_storage_generator_membership(context: PluginContext) -> None: """Create HeadStorage memberships between generators and head storages. @@ -281,6 +300,7 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: from r2x_sienna_to_plexos.getters import _build_generator_display_name_index display_name_index = _build_generator_display_name_index(context) + pumped_storage_target_names = _build_pumped_storage_target_generator_name_index(context) generators_by_name = {g.name: g for g in _target_system(context).get_components(PLEXOSGenerator)} storages_by_name = {s.name: s for s in _target_system(context).get_components(PLEXOSStorage)} @@ -332,6 +352,8 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: if target_gen is None: logger.debug("No PLEXOSGenerator found for HydroTurbine '{}', skipping.", tname) continue + if target_gen.name not in pumped_storage_target_names: + continue _ensure_membership(context, target_gen, target_storage, CollectionEnum.HeadStorage) total_memberships += 1 @@ -344,6 +366,8 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: target_gen = generators_by_name.get(target_gen_name) if target_gen is None: continue + if target_gen.name not in pumped_storage_target_names: + continue for reservoir in getattr(turbine, "reservoirs", None) or []: location = getattr(getattr(reservoir, "reservoir_location", None), "value", None) @@ -362,6 +386,8 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: # Fallback: For all generators and storages with matching _head names, ensure membership exists for gen_name, gen in generators_by_name.items(): + if gen_name not in pumped_storage_target_names: + continue if gen_name.endswith("_head"): storage = storages_by_name.get(gen_name) if storage is not None: @@ -386,6 +412,7 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: from r2x_sienna_to_plexos.getters import _build_generator_display_name_index display_name_index = _build_generator_display_name_index(context) + pumped_storage_target_names = _build_pumped_storage_target_generator_name_index(context) generators_by_name = {g.name: g for g in _target_system(context).get_components(PLEXOSGenerator)} storages_by_name = {s.name: s for s in _target_system(context).get_components(PLEXOSStorage)} @@ -437,6 +464,8 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: if target_gen is None: logger.debug("No PLEXOSGenerator found for HydroTurbine '{}', skipping.", tname) continue + if target_gen.name not in pumped_storage_target_names: + continue _ensure_membership(context, target_gen, target_storage, CollectionEnum.TailStorage) total_memberships += 1 @@ -449,6 +478,8 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: target_gen = generators_by_name.get(target_gen_name) if target_gen is None: continue + if target_gen.name not in pumped_storage_target_names: + continue for reservoir in getattr(turbine, "reservoirs", None) or []: location = getattr(getattr(reservoir, "reservoir_location", None), "value", None) @@ -467,6 +498,8 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: # Fallback: For all generators and storages with matching _tail names, ensure membership exists for gen_name, gen in generators_by_name.items(): + if gen_name not in pumped_storage_target_names: + continue if gen_name.endswith("_tail"): storage = storages_by_name.get(gen_name) if storage is not None: diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index 16d11a8b..6542df73 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -1050,7 +1050,9 @@ def test_membership_transformer_from_to_parent_node(context): assert getters.membership_transformer_to_parent_node(transformer, context).unwrap().name == "N2" -def test_membership_head_tail_storage_generator(context): +def test_membership_head_tail_storage_generator(context, monkeypatch): + monkeypatch.setattr(getters, "_is_hydro_pumped_storage_generator", lambda _ctx, _name: True) + bus1 = ACBus(name="N2", base_voltage=115.0, number=1) context.source_system.add_component(bus1) ht = HydroTurbine( diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index cd4f36ef..f97e06fa 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -265,7 +265,29 @@ def test_ensure_reference_node_memberships_skips_when_no_ref_bus(context): assert not any(m.collection == CollectionEnum.ReferenceNode for m in memberships) -def test_ensure_head_tail_storage_generator_membership(context): +def test_ensure_head_tail_storage_generator_membership(context, monkeypatch): + import r2x_sienna_to_plexos.getters as getters_mod + + monkey_source = types.SimpleNamespace(name="foo_head") + monkey_source_tail = types.SimpleNamespace(name="foo_tail") + + def monkeypatch_get_components(comp_type): + return ( + [monkey_source, monkey_source_tail] + if getattr(comp_type, "__name__", "") == "HydroPumpedStorage" + else [] + ) + + context.source_system.get_components = monkeypatch_get_components + monkeypatch.setattr( + getters_mod, + "_build_generator_display_name_index", + lambda _ctx: { + "foo_head": "foo_head", + "foo_tail": "foo_tail", + }, + ) + gen = PLEXOSGenerator(name="foo_head") storage = PLEXOSStorage(name="foo_head") context.target_system.add_component(gen) @@ -386,7 +408,20 @@ def test_ensure_battery_node_memberships(context): assert any(m.collection.name == "Nodes" for m in memberships) -def test_ensure_head_storage_generator_membership(context): +def test_ensure_head_storage_generator_membership(context, monkeypatch): + import r2x_sienna_to_plexos.getters as getters_mod + + context.source_system.get_components = ( + lambda comp_type: [types.SimpleNamespace(name="GEN_head")] + if getattr(comp_type, "__name__", "") == "HydroPumpedStorage" + else [] + ) + monkeypatch.setattr( + getters_mod, + "_build_generator_display_name_index", + lambda _ctx: {"GEN_head": "GEN_head"}, + ) + gen = PLEXOSGenerator(name="GEN_head") storage = PLEXOSStorage(name="GEN_head") context.target_system.add_component(gen) @@ -396,7 +431,20 @@ def test_ensure_head_storage_generator_membership(context): assert any(m.collection.name == "HeadStorage" for m in memberships) -def test_ensure_tail_storage_generator_membership(context): +def test_ensure_tail_storage_generator_membership(context, monkeypatch): + import r2x_sienna_to_plexos.getters as getters_mod + + context.source_system.get_components = ( + lambda comp_type: [types.SimpleNamespace(name="GEN_tail")] + if getattr(comp_type, "__name__", "") == "HydroPumpedStorage" + else [] + ) + monkeypatch.setattr( + getters_mod, + "_build_generator_display_name_index", + lambda _ctx: {"GEN_tail": "GEN_tail"}, + ) + gen = PLEXOSGenerator(name="GEN_tail") storage = PLEXOSStorage(name="GEN_tail") context.target_system.add_component(gen) @@ -825,8 +873,8 @@ def test_hydroturbine_driven_head_tail_memberships(context, monkeypatch): getters_utils.ensure_tail_storage_generator_membership(context) memberships = context.target_system.get_supplemental_attributes_with_component(gen, PLEXOSMembership) - assert any(m.collection == CollectionEnum.HeadStorage for m in memberships) - assert any(m.collection == CollectionEnum.TailStorage for m in memberships) + assert not any(m.collection == CollectionEnum.HeadStorage for m in memberships) + assert not any(m.collection == CollectionEnum.TailStorage for m in memberships) def test_generator_reserve_interface_and_battery_memberships(context, monkeypatch): @@ -1035,7 +1083,16 @@ def source_get_components(comp_type): def test_head_tail_memberships_from_ext_plants_and_fallback_name_matching(context, monkeypatch): import r2x_sienna_to_plexos.getters as getters_mod - monkeypatch.setattr(getters_mod, "_build_generator_display_name_index", lambda _ctx: {"T1": "GEN_T1"}) + monkeypatch.setattr( + getters_mod, + "_build_generator_display_name_index", + lambda _ctx: { + "T1": "GEN_T1", + "GEN_T1": "GEN_T1", + "Fallback_head": "Fallback_head", + "Fallback_tail": "Fallback_tail", + }, + ) reservoir = types.SimpleNamespace(name="ReservoirA", ext={"plant_name": "PlantA", "plants": ["T1"]}) turbine = types.SimpleNamespace(name="T1", reservoirs=[]) @@ -1046,6 +1103,12 @@ def source_get_components(comp_type): return [reservoir] if name == "HydroTurbine": return [turbine] + if name == "HydroPumpedStorage": + return [ + types.SimpleNamespace(name="GEN_T1"), + types.SimpleNamespace(name="Fallback_head"), + types.SimpleNamespace(name="Fallback_tail"), + ] return [] context.source_system.get_components = source_get_components From d6a3ba8a16f652b7536861fa9b64ce13b58ef582 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Tue, 21 Apr 2026 14:17:00 -0600 Subject: [PATCH 09/16] feat: resolve head/tail hps generator associations for translation --- .../src/r2x_sienna_to_plexos/getters.py | 36 ++++++++ .../tests/test_getters.py | 82 ++++++++++++++++++- .../test_translation_rule_application.py | 41 ++++++++++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index f4c948bd..d538b9ac 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -1708,11 +1708,40 @@ def get_turbine_pump_load( return Ok(0.0) +def _reservoir_has_hydro_pumped_storage_association( + source_component: HydroReservoir, context: PluginContext +) -> bool: + """Return True if reservoir is linked to HydroPumpTurbine and not HydroTurbine.""" + + def _is_hydro_pump_turbine(turbine: Any) -> bool: + return isinstance(turbine, HydroPumpTurbine) or type(turbine).__name__ == "HydroPumpTurbine" + + def _is_hydro_turbine(turbine: Any) -> bool: + return isinstance(turbine, HydroTurbine) or type(turbine).__name__ == "HydroTurbine" + + linked_turbines = [ + *list(getattr(source_component, "upstream_turbines", None) or []), + *list(getattr(source_component, "downstream_turbines", None) or []), + ] + + has_hydro_pump_turbine = any(_is_hydro_pump_turbine(turbine) for turbine in linked_turbines) + has_hydro_turbine = any(_is_hydro_turbine(turbine) for turbine in linked_turbines) + + return has_hydro_pump_turbine and not has_hydro_turbine + + @getter def get_head_storage_name( source_component: HydroReservoir, context: PluginContext ) -> Result[str, ValueError]: """Return the storage name for the head reservoir (appends _head), using plant_name from ext if available.""" + if not _reservoir_has_hydro_pumped_storage_association(source_component, context): + return Err( + ValueError( + f"Skipping head storage conversion for reservoir '{source_component.name}': no HydroPumpedStorage association" + ) + ) + ext = getattr(source_component, "ext", None) base = None if isinstance(ext, dict): @@ -1740,6 +1769,13 @@ def get_tail_storage_name( source_component: HydroReservoir, context: PluginContext ) -> Result[str, ValueError]: """Return the storage name for the tail reservoir (appends _tail), using plant_name from ext if available.""" + if not _reservoir_has_hydro_pumped_storage_association(source_component, context): + return Err( + ValueError( + f"Skipping tail storage conversion for reservoir '{source_component.name}': no HydroPumpedStorage association" + ) + ) + ext = getattr(source_component, "ext", None) base = None if isinstance(ext, dict): diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index 6542df73..adc0358e 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -495,7 +495,13 @@ def get_components(cls, filter_func=None): assert getters.get_area_load(acbus, context).unwrap() == 0.0 -def test_get_head_tail_storage_names_valid(context): +def test_get_head_tail_storage_names_valid(context, monkeypatch): + monkeypatch.setattr( + getters, + "_reservoir_has_hydro_pumped_storage_association", + lambda _source_component, _context: True, + ) + hydro = HydroReservoir( name="hydro-reservoir-test", available=True, @@ -896,7 +902,12 @@ def test_get_area_units_and_load(context): assert getters.get_area_load(area, context).unwrap() == 0.0 -def test_get_head_tail_storage_name(context): +def test_get_head_tail_storage_name(context, monkeypatch): + monkeypatch.setattr( + getters, + "_reservoir_has_hydro_pumped_storage_association", + lambda _source_component, _context: True, + ) hydro = HydroReservoir( name="hydro1", available=True, @@ -918,6 +929,57 @@ def test_get_head_tail_storage_name(context): assert getters.get_tail_storage_name(hydro, context).unwrap() == "hydro1_tail" +def test_get_head_tail_storage_name_err_without_pumped_storage_association(context): + hydro = HydroReservoir( + name="hydro1", + available=True, + storage_level_limits=MinMax(min=0.0, max=1000.0), + initial_level=0.5, + spillage_limits=MinMax(min=0.0, max=100.0), + inflow=50.0, + outflow=30.0, + level_targets=0.8, + travel_time=2.0, + intake_elevation=500.0, + head_to_volume_factor=LinearCurve(1.0), + reservoir_location=ReservoirLocation.HEAD, + operation_cost=HydroReservoirCost(), + level_data_type=ReservoirDataType.USABLE_VOLUME, + category="hydro_reservoir", + ) + + assert getters.get_head_storage_name(hydro, context).is_err() + assert getters.get_tail_storage_name(hydro, context).is_err() + + +def test_reservoir_association_true_for_hydropumpturbine_links(context): + pump_turbine = type("HydroPumpTurbine", (), {})() + reservoir = type( + "ReservoirProxy", + (), + { + "upstream_turbines": [pump_turbine], + "downstream_turbines": [], + }, + )() + + assert getters._reservoir_has_hydro_pumped_storage_association(reservoir, context) + + +def test_reservoir_association_false_for_hydroturbine_links(context): + hydro_turbine = type("HydroTurbine", (), {})() + reservoir = type( + "ReservoirProxy", + (), + { + "upstream_turbines": [], + "downstream_turbines": [hydro_turbine], + }, + )() + + assert not getters._reservoir_has_hydro_pumped_storage_association(reservoir, context) + + def test_membership_component_child_node_generator(context): gen = PLEXOSGenerator(name="GEN1") node = PLEXOSNode(name="N1") @@ -1558,7 +1620,13 @@ def test_get_area_load(context): assert getters.get_area_load(area, context).unwrap() == 0.0 -def test_get_head_storage_name(context): +def test_get_head_storage_name(context, monkeypatch): + monkeypatch.setattr( + getters, + "_reservoir_has_hydro_pumped_storage_association", + lambda _source_component, _context: True, + ) + hydro = HydroReservoir( name="hydro1_head", available=True, @@ -1576,7 +1644,13 @@ def test_get_head_storage_name(context): assert getters.get_head_storage_name(hydro, context).unwrap() == "hydro1_head" -def test_get_tail_storage_name(context): +def test_get_tail_storage_name(context, monkeypatch): + monkeypatch.setattr( + getters, + "_reservoir_has_hydro_pumped_storage_association", + lambda _source_component, _context: True, + ) + hydro = HydroReservoir( name="hydro1_tail", available=True, diff --git a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py index 3398583a..69cc3d34 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py +++ b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py @@ -165,6 +165,47 @@ def test_sienna_storage_translates_to_plexos_storage(tmp_path): assert storage +def test_hydro_reservoir_without_pumped_storage_does_not_translate_to_plexos_storage(tmp_path): + from infrasys.value_curves import LinearCurve + from r2x_plexos.models import PLEXOSStorage + from r2x_sienna.models import HydroReservoir + from r2x_sienna.models.costs import HydroReservoirCost + from r2x_sienna.models.enums import ReservoirDataType, ReservoirLocation + from r2x_sienna.models.named_tuples import MinMax + + context, rules = make_context_and_rules(tmp_path) + context.source_system = System(name="source", auto_add_composed_components=True) + context.source_system.add_component( + HydroReservoir( + name="EI_Reservoir", + available=True, + storage_level_limits=MinMax(min=0.0, max=1000.0), + initial_level=0.5, + spillage_limits=MinMax(min=0.0, max=100.0), + inflow=50.0, + outflow=30.0, + level_targets=0.8, + travel_time=2.0, + intake_elevation=500.0, + head_to_volume_factor=LinearCurve(1.0), + reservoir_location=ReservoirLocation.HEAD, + operation_cost=HydroReservoirCost(), + level_data_type=ReservoirDataType.USABLE_VOLUME, + category="hydro_reservoir", + ) + ) + context.target_system = System(name="target", auto_add_composed_components=True) + context.rules = rules + + result = apply_rules_to_context(context) + assert result.total_rules > 0 + + storages = list(context.target_system.get_components(PLEXOSStorage)) + assert not any( + s.name in {"EI_head", "EI_tail", "EI_Reservoir_head", "EI_Reservoir_tail"} for s in storages + ) + + def test_sienna_interface_translates_to_plexos_interface(tmp_path): from r2x_plexos.models import PLEXOSInterface from r2x_sienna.models import Area, TransmissionInterface From 05d1cf5cf1ae33289a23c051ea8ee9267e5e78f5 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Tue, 21 Apr 2026 14:26:19 -0600 Subject: [PATCH 10/16] fix: update reeds/sienna fuel type mapping --- .../src/r2x_sienna_to_plexos/config/defaults.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json index 4819e0bc..fb21c230 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json @@ -82,13 +82,9 @@ "WASTE_COAL", "SYNTHESIS_GAS_COAL" ], - "gas-cc": [ + "natural-gas": [ "NATURAL_GAS" ], - "gas-ct": [ - "OTHER_GAS", - "BLAST_FURNACE_GAS" - ], "o-g-s": [ "DISTILLATE_FUEL_OIL", "RESIDUAL_FUEL_OIL", @@ -97,7 +93,9 @@ "KEROSENE", "PROPANE", "WASTE_OIL", - "SYNTHESIS_GAS_PETROLEUM_COKE" + "SYNTHESIS_GAS_PETROLEUM_COKE", + "OTHER_GAS", + "BLAST_FURNACE_GAS" ], "biopower": [ "AG_BIOPRODUCT", From 8ad4a7fe9ed2c6b032280034d7e962353cf57d30 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Tue, 21 Apr 2026 14:40:41 -0600 Subject: [PATCH 11/16] fix: update reference node logic and turn off renewable dispatch gens when no time series are found --- .../r2x_sienna_to_plexos/config/defaults.json | 159 +----------------- .../r2x_sienna_to_plexos/config/rules.json | 6 + .../src/r2x_sienna_to_plexos/getters.py | 45 +++++ .../src/r2x_sienna_to_plexos/getters_utils.py | 68 ++++++-- .../tests/test_getters.py | 76 +++++++++ .../tests/test_getters_utils.py | 78 +++++++-- 6 files changed, 250 insertions(+), 182 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json index fb21c230..b5ca6f1f 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/defaults.json @@ -61,10 +61,10 @@ "ES": "other", "FC": "smr", "FW": "other", - "HA": "hydnd", - "HB": "hydnd", - "HK": "hydnd", - "HY": "hyded", + "HA": "hydro", + "HB": "hydro", + "HK": "hydro", + "HY": "hydro", "PS": "pumped-hydro", "OT": "other", "PVe": "upv", @@ -788,132 +788,6 @@ "ramp_rate_down": 30.0, "ramp_rate_up": 30.0 }, - "hydd": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": null, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hyded": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": null, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydend": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": 1.0, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydnd": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": 1.0, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydnpd": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": null, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydnpnd": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": 1.0, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydsd": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": null, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydsn": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": 1.0, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, - "hydud": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": null, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, "hydro": { "average_capacity_MW": 50.0, "forced_outage_rate": 0.02, @@ -925,31 +799,6 @@ "ramp_rate_up": 100.0, "vom_cost": 3.2 }, - "hydtrb": { - "average_capacity_MW": 50.0, - "forced_outage_rate": 0.02, - "maintenance_rate": 0.01, - "mean_time_to_repair": 48, - "min_energy": 0.0, - "min_power": 0.0, - "ramp_rate_down": 100.0, - "ramp_rate_up": 100.0, - "vom_cost": 3.2 - }, - "hydund": { - "capacity_MW": null, - "forced_outage_rate": null, - "maintenance_rate": null, - "max_capacity_MW": null, - "max_ramp_up_percentage": 0.05, - "mean_time_to_repair": 24.0, - "min_capacity_MW": null, - "min_down_time": null, - "min_stable_level_percentage": 1.0, - "min_up_time": null, - "start_cost_per_MW": null, - "vom_cost": 3.2 - }, "lfill-gas": { "capacity_MW": 20.0, "forced_outage_rate": 0.0309, diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index af4157c9..54e875dd 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -441,6 +441,7 @@ }, "getters": { "name": "get_generator_name", + "units": "get_dispatch_generator_units", "commit": "get_generator_commit", "category": "get_generator_category", "max_ramp_down": "get_max_ramp_down", @@ -470,6 +471,7 @@ }, "getters": { "name": "get_generator_name", + "units": "get_dispatch_generator_units", "commit": "get_generator_commit", "category": "get_generator_category", "max_ramp_down": "get_max_ramp_down", @@ -498,6 +500,7 @@ }, "getters": { "name": "get_generator_name", + "units": "get_dispatch_generator_units", "commit": "get_generator_commit", "rating": "get_generator_rating", "max_ramp_up": "get_max_ramp_up", @@ -524,6 +527,7 @@ }, "getters": { "name": "get_generator_name", + "units": "get_dispatch_generator_units", "commit": "get_generator_commit", "category": "get_generator_category", "rating": "get_generator_rating", @@ -553,6 +557,7 @@ }, "getters": { "name": "get_generator_name", + "units": "get_dispatch_generator_units", "commit": "get_generator_commit", "category": "get_generator_category", "rating": "get_generator_rating", @@ -579,6 +584,7 @@ "getters": { "commit": "get_generator_commit", "name": "get_generator_name", + "units": "get_dispatch_generator_units", "category": "get_generator_category", "rating": "get_generator_rating", "max_capacity": "get_max_capacity", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index d538b9ac..e189e506 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -633,6 +633,34 @@ def _attach_generator_time_series( logger.success("Attached time series {} to generator {}", ts.name, generator_name) +def _has_usable_generator_time_series(source_component: object, context: PluginContext) -> bool: + """Return True when the source generator has at least one retrievable time series.""" + source_system = _source_system(context) + + try: + if not source_system.time_series.has_time_series(source_component): + return False + metadata_items = source_system.time_series.list_time_series_metadata(source_component) + except Exception: + # If introspection fails, avoid accidentally deactivating the unit. + return True + + for metadata in metadata_items: + features = getattr(metadata, "features", {}) or {} + try: + ts_list = source_system.list_time_series( + source_component, + name=metadata.name, + **features, + ) + except Exception: + continue + if ts_list: + return True + + return False + + def _attach_region_node_load_time_series( context: PluginContext, region_name: str, @@ -1320,6 +1348,7 @@ def get_thermal_generator_units( If fuel price or heat rate resolves to zero, set units to 0 so the device is not treated as nearly free generation in PLEXOS. + Also deactivate units that have no associated source time series. """ ext = getattr(source_component, "ext", None) if isinstance(ext, dict): @@ -1351,9 +1380,25 @@ def get_thermal_generator_units( ): return Ok(0) + if not _has_usable_generator_time_series(source_component, context): + return Ok(0) + return Ok(1) +@getter +def get_dispatch_generator_units( + source_component: HydroDispatch + | HydroTurbine + | HydroPumpTurbine + | RenewableDispatch + | RenewableNonDispatch, + context: PluginContext, +) -> Result[int, ValueError]: + """Deactivate dispatch generators that do not have source time series.""" + return Ok(1 if _has_usable_generator_time_series(source_component, context) else 0) + + @getter def get_max_capacity(source_component: object, context: PluginContext) -> Result[float, ValueError]: """Extract maximum capacity in MW from rating, active_power_limits, or max_active_power. diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index 808a53c2..843df43a 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -234,34 +234,70 @@ def ensure_region_node_memberships(context: PluginContext) -> None: def ensure_reference_node_memberships(context: PluginContext) -> None: - """Create Region->Node memberships in ReferenceNode for REF/SLACK bus regions only.""" + """Create exactly one Region->Node ReferenceNode membership per translated region. + + Selection priority per region: + 1) Any node whose source bus is REF/SLACK + 2) Fallback to highest node voltage, then highest load participation factor + """ + + def _as_float(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return 0.0 + bus_index = _bus_name_to_area_and_zone(context) regions_by_name = {r.name: r for r in _target_system(context).get_components(PLEXOSRegion)} - ref_region_names: set[str] = set() + # Build fast lookup for source bus by node name. + source_buses_by_name = {bus.name: bus for bus in _source_system(context).get_components(ACBus)} + + nodes_by_region: dict[str, list[PLEXOSNode]] = {} + for node in _target_system(context).get_components(PLEXOSNode): + area_name, _ = bus_index.get(node.name, (None, None)) + if area_name is None: + continue + nodes_by_region.setdefault(area_name, []).append(node) + + ref_node_names: set[str] = set() for bus in _source_system(context).get_components(ACBus): bustype = getattr(bus, "bustype", None) bustype_name = getattr(bustype, "name", str(bustype)).upper() if bustype is not None else "" if bustype not in {ACBusTypes.REF, ACBusTypes.SLACK} and bustype_name not in {"REF", "SLACK"}: continue - area_name, _ = bus_index.get(bus.name, (None, None)) - if area_name is not None: - ref_region_names.add(area_name) - - if not ref_region_names: - logger.info("No REF/SLACK buses found. Skipping ReferenceNode memberships.") - return + ref_node_names.add(bus.name) total_memberships = 0 - for node in _target_system(context).get_components(PLEXOSNode): - area_name, _ = bus_index.get(node.name, (None, None)) - if area_name is None or area_name not in ref_region_names: + for region_name, region in regions_by_name.items(): + region_nodes = nodes_by_region.get(region_name, []) + if not region_nodes: continue - region = regions_by_name.get(area_name) - if region is not None: - _ensure_membership(context, region, node, CollectionEnum.ReferenceNode) - total_memberships += 1 + + slack_nodes = [node for node in region_nodes if node.name in ref_node_names] + candidate_nodes = slack_nodes if slack_nodes else region_nodes + + chosen = max( + candidate_nodes, + key=lambda node: ( + _as_float(getattr(node, "voltage", 0.0)), + _as_float(getattr(node, "load_participation_factor", 0.0)), + node.name, + ), + ) + + _ensure_membership(context, region, chosen, CollectionEnum.ReferenceNode) + total_memberships += 1 + + if not slack_nodes: + source_bus = source_buses_by_name.get(chosen.name) + bus_label = chosen.name if source_bus is None else source_bus.name + logger.debug( + "No REF/SLACK bus found for region '{}'; using fallback reference node '{}'.", + region_name, + bus_label, + ) logger.info("Total {} ReferenceNode Region->Node memberships created.", total_memberships) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index adc0358e..4b06edb6 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -638,6 +638,14 @@ def test_get_thermal_generator_units_zero_when_fuel_price_zero(monkeypatch, cont class DummyThermal: pass + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(0.0)) monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(9.5)) @@ -648,6 +656,14 @@ def test_get_thermal_generator_units_zero_when_heat_rate_zero(monkeypatch, conte class DummyThermal: pass + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(2.3)) monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(0.0)) @@ -658,6 +674,14 @@ def test_get_thermal_generator_units_one_when_inputs_present(monkeypatch, contex class DummyThermal: pass + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(2.3)) monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(9.5)) @@ -668,6 +692,14 @@ def test_get_thermal_generator_units_zero_for_monticello_tx(monkeypatch, context class DummyThermal: ext = {"plant_name": "Monticello", "state": "TX"} # noqa: RUF012 + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(2.3)) monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(9.5)) @@ -678,12 +710,56 @@ def test_get_thermal_generator_units_keeps_monticello_mn_active(monkeypatch, con class DummyThermal: ext = {"plant_name": "Monticello Nuclear Facility", "state": "MN"} # noqa: RUF012 + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(2.3)) monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(9.5)) assert getters.get_thermal_generator_units(DummyThermal(), context).unwrap() == 1 +def test_get_thermal_generator_units_zero_when_time_series_missing(monkeypatch, context): + class DummyThermal: + pass + + context.source_system.time_series.has_time_series = lambda _component: False + + monkeypatch.setattr(getters, "get_fuel_price", lambda *_: Ok(2.3)) + monkeypatch.setattr(getters, "get_heat_rate", lambda *_: Ok(9.5)) + + assert getters.get_thermal_generator_units(DummyThermal(), context).unwrap() == 0 + + +def test_get_dispatch_generator_units_zero_when_time_series_missing(context): + class DummyDispatch: + pass + + context.source_system.time_series.has_time_series = lambda _component: False + + assert getters.get_dispatch_generator_units(DummyDispatch(), context).unwrap() == 0 + + +def test_get_dispatch_generator_units_one_when_time_series_present(context): + class DummyDispatch: + pass + + context.source_system.time_series.has_time_series = lambda _component: True + context.source_system.time_series.list_time_series_metadata = lambda _component: [ + types.SimpleNamespace(name="max_active_power", features={}) + ] + context.source_system.list_time_series = ( + lambda _component, **kwargs: [object()] if kwargs.get("name") else [] + ) + + assert getters.get_dispatch_generator_units(DummyDispatch(), context).unwrap() == 1 + + def _make_thermal_generator_for_category_tests( name: str, fuel: ThermalFuels | str, diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index f97e06fa..90b49954 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -211,13 +211,13 @@ def test_ensure_transformer_node_memberships(context): assert any(m.collection in (CollectionEnum.NodeFrom, CollectionEnum.NodeTo) for m in memberships) -def test_ensure_reference_node_memberships_only_for_ref_region(context): +def test_ensure_reference_node_memberships_creates_one_per_region(context): area_ref = Area(name="A1") area_other = Area(name="A2") region_ref = PLEXOSRegion(name="A1") region_other = PLEXOSRegion(name="A2") - node_ref = PLEXOSNode(name="N1") - node_other = PLEXOSNode(name="N2") + node_ref = PLEXOSNode(name="N1", voltage=138.0, load_participation_factor=0.3) + node_other = PLEXOSNode(name="N2", voltage=230.0, load_participation_factor=0.2) bus_ref = ACBus(name="N1", number=1, bustype=ACBusTypes.REF, area=area_ref) bus_other = ACBus(name="N2", number=2, bustype=ACBusTypes.PQ, area=area_other) @@ -245,24 +245,80 @@ def test_ensure_reference_node_memberships_only_for_ref_region(context): and m.child_object == node_ref for m in ref_memberships ) - assert not any(m.collection == CollectionEnum.ReferenceNode for m in other_memberships) + assert any( + m.collection == CollectionEnum.ReferenceNode + and m.parent_object == region_other + and m.child_object == node_other + for m in other_memberships + ) -def test_ensure_reference_node_memberships_skips_when_no_ref_bus(context): +def test_ensure_reference_node_memberships_prefers_slack_bus_when_present(context): area = Area(name="A1") region = PLEXOSRegion(name="A1") - node = PLEXOSNode(name="N1") - bus = ACBus(name="N1", number=1, bustype=ACBusTypes.PQ, area=area) + node_slack = PLEXOSNode(name="N1", voltage=115.0, load_participation_factor=0.1) + node_non_slack = PLEXOSNode(name="N2", voltage=500.0, load_participation_factor=0.9) + bus_slack = ACBus(name="N1", number=1, bustype=ACBusTypes.REF, area=area) + bus_non_slack = ACBus(name="N2", number=2, bustype=ACBusTypes.PQ, area=area) context.source_system.add_component(area) context.target_system.add_component(region) - context.target_system.add_component(node) - context.source_system.add_component(bus) + context.target_system.add_component(node_slack) + context.target_system.add_component(node_non_slack) + context.source_system.add_component(bus_slack) + context.source_system.add_component(bus_non_slack) getters_utils.ensure_reference_node_memberships(context) - memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) - assert not any(m.collection == CollectionEnum.ReferenceNode for m in memberships) + slack_memberships = context.target_system.get_supplemental_attributes_with_component( + node_slack, PLEXOSMembership + ) + non_slack_memberships = context.target_system.get_supplemental_attributes_with_component( + node_non_slack, PLEXOSMembership + ) + + assert any( + m.collection == CollectionEnum.ReferenceNode + and m.parent_object == region + and m.child_object == node_slack + for m in slack_memberships + ) + assert not any( + m.collection == CollectionEnum.ReferenceNode and m.parent_object == region + for m in non_slack_memberships + ) + + +def test_ensure_reference_node_memberships_fallback_uses_voltage_then_lpf(context): + area = Area(name="A1") + region = PLEXOSRegion(name="A1") + node_low = PLEXOSNode(name="N1", voltage=115.0, load_participation_factor=0.9) + node_mid = PLEXOSNode(name="N2", voltage=230.0, load_participation_factor=0.1) + node_best = PLEXOSNode(name="N3", voltage=230.0, load_participation_factor=0.6) + bus_low = ACBus(name="N1", number=1, bustype=ACBusTypes.PQ, area=area) + bus_mid = ACBus(name="N2", number=2, bustype=ACBusTypes.PQ, area=area) + bus_best = ACBus(name="N3", number=3, bustype=ACBusTypes.PQ, area=area) + + context.source_system.add_component(area) + context.target_system.add_component(region) + context.target_system.add_component(node_low) + context.target_system.add_component(node_mid) + context.target_system.add_component(node_best) + context.source_system.add_component(bus_low) + context.source_system.add_component(bus_mid) + context.source_system.add_component(bus_best) + + getters_utils.ensure_reference_node_memberships(context) + + best_memberships = context.target_system.get_supplemental_attributes_with_component( + node_best, PLEXOSMembership + ) + assert any( + m.collection == CollectionEnum.ReferenceNode + and m.parent_object == region + and m.child_object == node_best + for m in best_memberships + ) def test_ensure_head_tail_storage_generator_membership(context, monkeypatch): From d1ece633ed48915237f51681dc2839488796fa75 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Tue, 21 Apr 2026 17:21:13 -0600 Subject: [PATCH 12/16] fix: re-add load zone objects for plexos zones in translation --- .../src/r2x_sienna_to_plexos/__init__.py | 2 + .../r2x_sienna_to_plexos/config/rules.json | 16 ++++++++ .../src/r2x_sienna_to_plexos/getters.py | 7 ++++ .../src/r2x_sienna_to_plexos/getters_utils.py | 22 ++++++++++ .../src/r2x_sienna_to_plexos/translation.py | 2 + .../tests/test_getters_utils.py | 41 +++++++++++++++++++ 6 files changed, 90 insertions(+) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py index 5c36ead8..af8ad1e9 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py @@ -33,6 +33,7 @@ ensure_reserve_time_series, ensure_tail_storage_generator_membership, ensure_transformer_node_memberships, + ensure_zone_node_memberships, ) from .plugin_config import SiennaToPlexosConfig from .translation import sienna_to_plexos @@ -51,6 +52,7 @@ "sienna_to_plexos", "REEDS_COMPONENT_SUBSTRINGS", "ensure_region_node_memberships", + "ensure_zone_node_memberships", "ensure_reference_node_memberships", "ensure_interface_line_memberships", "ensure_generator_node_memberships", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index 54e875dd..f3f84bd7 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -36,6 +36,22 @@ "target_type": "PLEXOSRegion", "version": 1 }, + { + "defaults": { + "category": "zones" + }, + "field_map": { + "name": "name", + "uuid": "uuid", + "category": "category" + }, + "getters": { + "units": "get_zone_units" + }, + "source_type": "LoadZone", + "target_type": "PLEXOSZone", + "version": 1 + }, { "defaults": { "category": "line", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index e189e506..aac3d79c 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -33,6 +33,7 @@ HydroReservoir, HydroTurbine, Line, + LoadZone, MonitoredLine, PhaseShiftingTransformer, PhaseShiftingTransformer3W, @@ -1018,6 +1019,12 @@ def get_area_name(source_component: Area, context: PluginContext) -> Result[str, return Ok(getattr(source_component, "name", "")) +@getter +def get_zone_units(source_component: LoadZone, context: PluginContext) -> Result[float, ValueError]: + """Return active status for translated zones.""" + return Ok(1.0) + + @getter def get_line_min_flow( source_component: Line diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index 843df43a..96a02e1f 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -22,6 +22,7 @@ PLEXOSReserve, PLEXOSStorage, PLEXOSTransformer, + PLEXOSZone, ) from r2x_sienna.models import ( ACBus, @@ -233,6 +234,27 @@ def ensure_region_node_memberships(context: PluginContext) -> None: logger.info("Total {} Region-Node memberships created.", total_memberships) +def ensure_zone_node_memberships(context: PluginContext) -> None: + """Create Zone memberships for nodes based on source bus load_zone mapping.""" + bus_index = _bus_name_to_area_and_zone(context) + zones_by_name = {z.name: z for z in _target_system(context).get_components(PLEXOSZone)} + + total_memberships = 0 + for node in _target_system(context).get_components(PLEXOSNode): + _, zone_name = bus_index.get(node.name, (None, None)) + if zone_name is None: + continue + + zone = zones_by_name.get(zone_name) + if zone is None: + continue + + _ensure_membership(context, node, zone, CollectionEnum.Zone) + total_memberships += 1 + + logger.info("Total {} Zone-Node memberships created.", total_memberships) + + def ensure_reference_node_memberships(context: PluginContext) -> None: """Create exactly one Region->Node ReferenceNode membership per translated region. diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py index 7a857131..78445614 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py @@ -24,6 +24,7 @@ ensure_reserve_time_series, ensure_tail_storage_generator_membership, ensure_transformer_node_memberships, + ensure_zone_node_memberships, ) @@ -60,6 +61,7 @@ def sienna_to_plexos(system: System, config: SiennaToPlexosConfig) -> System: ensure_generator_time_series(context) ensure_reserve_time_series(context) ensure_region_node_memberships(context) + ensure_zone_node_memberships(context) ensure_reference_node_memberships(context) ensure_generator_node_memberships(context) ensure_battery_node_memberships(context) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index 90b49954..0a2acfde 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -20,12 +20,14 @@ PLEXOSReserve, PLEXOSStorage, PLEXOSTransformer, + PLEXOSZone, ) from r2x_sienna.models import ( ACBus, Arc, Area, EnergyReservoirStorage, + LoadZone, ThermalStandard, Transformer2W, VariableReserve, @@ -179,6 +181,45 @@ def test_ensure_region_node_memberships(context): assert any(m.collection == CollectionEnum.Region for m in memberships) +def test_ensure_zone_node_memberships(context): + area = Area(name="A1") + zone = LoadZone(name="Z1") + node = PLEXOSNode(name="N1") + target_zone = PLEXOSZone(name="Z1") + bus = ACBus(name="N1", area=area, load_zone=zone, number=1) + + context.source_system.add_component(area) + context.source_system.add_component(zone) + context.source_system.add_component(bus) + context.target_system.add_component(node) + context.target_system.add_component(target_zone) + + getters_utils.ensure_zone_node_memberships(context) + + memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) + assert any( + m.collection == CollectionEnum.Zone and m.parent_object == node and m.child_object == target_zone + for m in memberships + ) + + +def test_ensure_zone_node_memberships_skips_missing_target_zone(context): + area = Area(name="A1") + zone = LoadZone(name="Z1") + node = PLEXOSNode(name="N1") + bus = ACBus(name="N1", area=area, load_zone=zone, number=1) + + context.source_system.add_component(area) + context.source_system.add_component(zone) + context.source_system.add_component(bus) + context.target_system.add_component(node) + + getters_utils.ensure_zone_node_memberships(context) + + memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) + assert not any(m.collection == CollectionEnum.Zone for m in memberships) + + def test_ensure_transformer_node_memberships(context): node1 = PLEXOSNode(name="N1") node2 = PLEXOSNode(name="N2") From dbcfbb33b05c17ccaa0123c37ed954fe893c56ae Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 22 Apr 2026 15:55:13 -0600 Subject: [PATCH 13/16] fix: implement updates on the storage head/tail to correctly translate associations --- .../src/r2x_sienna_to_plexos/__init__.py | 2 - .../r2x_sienna_to_plexos/config/rules.json | 46 +- .../src/r2x_sienna_to_plexos/getters.py | 183 +++- .../src/r2x_sienna_to_plexos/getters_utils.py | 68 +- .../src/r2x_sienna_to_plexos/translation.py | 2 - .../tests/test_getters.py | 147 ++- .../tests/test_getters_utils.py | 54 +- .../test_translation_rule_application.py | 7 +- uv.lock | 957 +++++++++++++++++- 9 files changed, 1287 insertions(+), 179 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py index af8ad1e9..5c36ead8 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py @@ -33,7 +33,6 @@ ensure_reserve_time_series, ensure_tail_storage_generator_membership, ensure_transformer_node_memberships, - ensure_zone_node_memberships, ) from .plugin_config import SiennaToPlexosConfig from .translation import sienna_to_plexos @@ -52,7 +51,6 @@ "sienna_to_plexos", "REEDS_COMPONENT_SUBSTRINGS", "ensure_region_node_memberships", - "ensure_zone_node_memberships", "ensure_reference_node_memberships", "ensure_interface_line_memberships", "ensure_generator_node_memberships", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index f3f84bd7..a7de8aea 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -733,6 +733,18 @@ "target_type": "PLEXOSMembership", "version": 1 }, + { + "name": "zone_node_membership", + "system": "target", + "getters": { + "parent_object": "membership_parent_component", + "child_object": "membership_node_child_zone", + "collection": "membership_collection_zone" + }, + "source_type": "PLEXOSNode", + "target_type": "PLEXOSMembership", + "version": 1 + }, { "name": "reserve_generator_membership", "system": "target", @@ -781,40 +793,6 @@ "target_type": "PLEXOSMembership", "version": 2 }, - { - "name": "head_storage_membership", - "system": "target", - "getters": { - "parent_object": "membership_parent_component", - "child_object": "membership_head_storage_generator", - "collection": "membership_collection_head_storage" - }, - "source_type": "PLEXOSGenerator", - "target_type": "PLEXOSMembership", - "version": 2, - "filter": { - "field": "category", - "op": "in", - "values": ["pumped-hydro", "hydtrb"] - } - }, - { - "name": "tail_storage_membership", - "system": "target", - "getters": { - "parent_object": "membership_parent_component", - "child_object": "membership_tail_storage_generator", - "collection": "membership_collection_tail_storage" - }, - "source_type": "PLEXOSGenerator", - "target_type": "PLEXOSMembership", - "version": 3, - "filter": { - "field": "category", - "op": "in", - "values": ["pumped-hydro", "hydtrb"] - } - }, { "name": "transformer_from_node_membership", "system": "target", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index aac3d79c..cf164308 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -21,6 +21,7 @@ PLEXOSNode, PLEXOSStorage, PLEXOSTransformer, + PLEXOSZone, ) from r2x_sienna.models import ( ACBus, @@ -1782,26 +1783,107 @@ def _is_hydro_turbine(turbine: Any) -> bool: return has_hydro_pump_turbine and not has_hydro_turbine +def _get_reservoir_location(source_component: HydroReservoir) -> str | None: + """Return normalized reservoir location label (HEAD/TAIL) when available. + + Falls back to ext metadata and name suffixes when explicit reservoir_location + is missing in source data. + """ + # Most reliable signal in EI data: explicit _head/_tail suffix in component name. + name = str(getattr(source_component, "name", "")).strip().upper() + if name.endswith(("_HEAD", " HEAD")): + return "HEAD" + if name.endswith(("_TAIL", " TAIL")): + return "TAIL" + + location = getattr(source_component, "reservoir_location", None) + raw = getattr(location, "value", location) + if raw is not None: + label = str(raw).upper() + if "HEAD" in label: + return "HEAD" + if "TAIL" in label: + return "TAIL" + + ext = getattr(source_component, "ext", None) + if isinstance(ext, dict): + ext_loc = ext.get("reservoir_location") or ext.get("RESERVOIR_LOCATION") + if ext_loc is not None: + label = str(getattr(ext_loc, "value", ext_loc)).upper() + if "HEAD" in label: + return "HEAD" + if "TAIL" in label: + return "TAIL" + + return None + + +def _get_reservoir_name_suffix_location(source_component: HydroReservoir) -> str | None: + """Return HEAD/TAIL when reservoir name explicitly ends with _head/_tail.""" + name = str(getattr(source_component, "name", "")).strip().casefold() + if name.endswith(("_head", " head")): + return "HEAD" + if name.endswith(("_tail", " tail")): + return "TAIL" + return None + + +def _get_reservoir_storage_base_name(source_component: HydroReservoir) -> str: + """Return canonical storage base name for a reservoir.""" + ext = getattr(source_component, "ext", None) + if isinstance(ext, dict): + plant_name = ext.get("plant_name") + if plant_name: + return str(plant_name) + return _reservoir_base_name(source_component.name) + + +def _has_explicit_side_reservoir_for_base( + source_component: HydroReservoir, + context: PluginContext, + side: str, +) -> bool: + """Return True when another reservoir with same base explicitly maps the requested side.""" + this_base = _get_reservoir_storage_base_name(source_component).casefold() + this_uuid = getattr(source_component, "uuid", None) + + for other in _source_system(context).get_components(HydroReservoir): + other_uuid = getattr(other, "uuid", None) + if this_uuid is not None and other_uuid == this_uuid: + continue + if _get_reservoir_storage_base_name(other).casefold() != this_base: + continue + if _get_reservoir_name_suffix_location(other) == side: + return True + + return False + + @getter def get_head_storage_name( source_component: HydroReservoir, context: PluginContext ) -> Result[str, ValueError]: """Return the storage name for the head reservoir (appends _head), using plant_name from ext if available.""" - if not _reservoir_has_hydro_pumped_storage_association(source_component, context): + # Only explicit suffixes gate conversion. Unsuffixed reservoirs are expanded + # into both _head and _tail storages. + suffix_location = _get_reservoir_name_suffix_location(source_component) + if suffix_location == "TAIL": return Err( ValueError( - f"Skipping head storage conversion for reservoir '{source_component.name}': no HydroPumpedStorage association" + f"Skipping head storage conversion for reservoir '{source_component.name}': name indicates tail reservoir" ) ) - ext = getattr(source_component, "ext", None) - base = None - if isinstance(ext, dict): - plant_name = ext.get("plant_name") - if plant_name: - base = str(plant_name) - if base is None: - base = _reservoir_base_name(source_component.name) + if suffix_location is None and _has_explicit_side_reservoir_for_base( + source_component, context, side="HEAD" + ): + return Err( + ValueError( + f"Skipping head storage conversion for reservoir '{source_component.name}': explicit head reservoir already exists for this plant" + ) + ) + + base = _get_reservoir_storage_base_name(source_component) return Ok(f"{base}_head") @@ -1821,21 +1903,26 @@ def get_tail_storage_name( source_component: HydroReservoir, context: PluginContext ) -> Result[str, ValueError]: """Return the storage name for the tail reservoir (appends _tail), using plant_name from ext if available.""" - if not _reservoir_has_hydro_pumped_storage_association(source_component, context): + # Only explicit suffixes gate conversion. Unsuffixed reservoirs are expanded + # into both _head and _tail storages. + suffix_location = _get_reservoir_name_suffix_location(source_component) + if suffix_location == "HEAD": return Err( ValueError( - f"Skipping tail storage conversion for reservoir '{source_component.name}': no HydroPumpedStorage association" + f"Skipping tail storage conversion for reservoir '{source_component.name}': name indicates head reservoir" ) ) - ext = getattr(source_component, "ext", None) - base = None - if isinstance(ext, dict): - plant_name = ext.get("plant_name") - if plant_name: - base = str(plant_name) - if base is None: - base = _reservoir_base_name(source_component.name) + if suffix_location is None and _has_explicit_side_reservoir_for_base( + source_component, context, side="TAIL" + ): + return Err( + ValueError( + f"Skipping tail storage conversion for reservoir '{source_component.name}': explicit tail reservoir already exists for this plant" + ) + ) + + base = _get_reservoir_storage_base_name(source_component) return Ok(f"{base}_tail") @@ -2161,6 +2248,14 @@ def membership_collection_region( return Ok(CollectionEnum.Region) +@getter +def membership_collection_zone( + component: object, context: PluginContext +) -> Result[CollectionEnum, ValueError]: + """Return the Zone collection enum.""" + return Ok(CollectionEnum.Zone) + + @getter def membership_collection_node_from( component: object, context: PluginContext @@ -2334,6 +2429,42 @@ def membership_region_child_node(region: object, context: PluginContext) -> Resu return Err(ValueError(f"Unexpected result type for region '{region_name}'")) +@getter +def membership_node_child_zone(node: PLEXOSNode, context: PluginContext) -> Result[PLEXOSZone, ValueError]: + """Resolve a node's source bus load_zone to the translated PLEXOSZone.""" + source_bus = _build_bus_name_index(context).get(getattr(node, "name", "")) + if source_bus is None: + return Err(ValueError(f"No source bus found for node '{getattr(node, 'name', '')}'")) + + load_zone = getattr(source_bus, "load_zone", None) + if load_zone is None: + area = getattr(source_bus, "area", None) + load_zone = getattr(area, "load_zone", None) if area is not None else None + if load_zone is None: + return Err(ValueError(f"No load_zone found for source bus '{source_bus.name}'")) + + zone_name = getattr(load_zone, "name", None) + zone_uuid = getattr(load_zone, "uuid", None) + + target_zones = list(_target_system(context).get_components(PLEXOSZone)) + if zone_name is not None: + for zone in target_zones: + if getattr(zone, "name", None) == str(zone_name): + return Ok(zone) + + if zone_uuid is not None: + zone_uuid_str = str(zone_uuid) + for zone in target_zones: + if str(getattr(zone, "uuid", "")) == zone_uuid_str: + return Ok(zone) + + return Err( + ValueError( + f"No translated PLEXOSZone found for bus '{source_bus.name}' load_zone '{zone_name or zone_uuid}'" + ) + ) + + @getter def membership_line_from_parent_node( line: PLEXOSLine, context: PluginContext @@ -2457,12 +2588,6 @@ def membership_head_storage_generator( generator: HydroTurbine, context: PluginContext ) -> Result[Any, ValueError]: gen_name = getattr(generator, "name", "") - if not _is_hydro_pumped_storage_generator(context, gen_name): - return Err( - ValueError( - f"Skipping HeadStorage membership for '{gen_name}': source generator is not HydroPumpedStorage" - ) - ) storage_index = _build_target_storage_name_index(context) # Primary: look up which reservoir owns this turbine @@ -2499,12 +2624,6 @@ def membership_tail_storage_generator( generator: HydroTurbine, context: PluginContext ) -> Result[Any, ValueError]: gen_name = getattr(generator, "name", "") - if not _is_hydro_pumped_storage_generator(context, gen_name): - return Err( - ValueError( - f"Skipping TailStorage membership for '{gen_name}': source generator is not HydroPumpedStorage" - ) - ) storage_index = _build_target_storage_name_index(context) # Primary: look up which reservoir owns this turbine diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index 96a02e1f..decbfa32 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -22,13 +22,11 @@ PLEXOSReserve, PLEXOSStorage, PLEXOSTransformer, - PLEXOSZone, ) from r2x_sienna.models import ( ACBus, Area, EnergyReservoirStorage, - HydroPumpedStorage, HydroReservoir, HydroTurbine, LoadZone, @@ -119,9 +117,16 @@ def _bus_name_to_area_and_zone(context: PluginContext) -> dict[str, tuple[str | area_name = str(area) load_zone = getattr(bus, "load_zone", None) - zone_name: str | None = ( - load_zone.name if isinstance(load_zone, LoadZone) else (str(load_zone) if load_zone else None) - ) + zone_name: str | None = None + if load_zone: + if isinstance(load_zone, LoadZone): + zone_name = load_zone.name + elif hasattr(load_zone, "name") and load_zone.name: + zone_name = str(load_zone.name) + elif isinstance(load_zone, str): + zone_name = load_zone + else: + zone_name = str(load_zone) result[bus.name] = (area_name, zone_name) context._cache["bus_name_to_area_and_zone"] = result @@ -234,27 +239,6 @@ def ensure_region_node_memberships(context: PluginContext) -> None: logger.info("Total {} Region-Node memberships created.", total_memberships) -def ensure_zone_node_memberships(context: PluginContext) -> None: - """Create Zone memberships for nodes based on source bus load_zone mapping.""" - bus_index = _bus_name_to_area_and_zone(context) - zones_by_name = {z.name: z for z in _target_system(context).get_components(PLEXOSZone)} - - total_memberships = 0 - for node in _target_system(context).get_components(PLEXOSNode): - _, zone_name = bus_index.get(node.name, (None, None)) - if zone_name is None: - continue - - zone = zones_by_name.get(zone_name) - if zone is None: - continue - - _ensure_membership(context, node, zone, CollectionEnum.Zone) - total_memberships += 1 - - logger.info("Total {} Zone-Node memberships created.", total_memberships) - - def ensure_reference_node_memberships(context: PluginContext) -> None: """Create exactly one Region->Node ReferenceNode membership per translated region. @@ -331,24 +315,6 @@ def _extract_base_name(name: str) -> str: return name -def _build_pumped_storage_target_generator_name_index(context: PluginContext) -> set[str]: - """Return target generator names that originate from Sienna HydroPumpedStorage.""" - cache_key = "pumped_storage_target_generator_name_index" - cached = context._cache.get(cache_key) - if cached is not None: - return cast(set[str], cached) - - from r2x_sienna_to_plexos.getters import _build_generator_display_name_index - - display_name_index = _build_generator_display_name_index(context) - allowed_names = { - display_name_index.get(source_gen.name, source_gen.name) - for source_gen in _source_system(context).get_components(HydroPumpedStorage) - } - context._cache[cache_key] = allowed_names - return allowed_names - - def ensure_head_storage_generator_membership(context: PluginContext) -> None: """Create HeadStorage memberships between generators and head storages. @@ -358,7 +324,6 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: from r2x_sienna_to_plexos.getters import _build_generator_display_name_index display_name_index = _build_generator_display_name_index(context) - pumped_storage_target_names = _build_pumped_storage_target_generator_name_index(context) generators_by_name = {g.name: g for g in _target_system(context).get_components(PLEXOSGenerator)} storages_by_name = {s.name: s for s in _target_system(context).get_components(PLEXOSStorage)} @@ -410,8 +375,6 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: if target_gen is None: logger.debug("No PLEXOSGenerator found for HydroTurbine '{}', skipping.", tname) continue - if target_gen.name not in pumped_storage_target_names: - continue _ensure_membership(context, target_gen, target_storage, CollectionEnum.HeadStorage) total_memberships += 1 @@ -424,8 +387,6 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: target_gen = generators_by_name.get(target_gen_name) if target_gen is None: continue - if target_gen.name not in pumped_storage_target_names: - continue for reservoir in getattr(turbine, "reservoirs", None) or []: location = getattr(getattr(reservoir, "reservoir_location", None), "value", None) @@ -444,8 +405,6 @@ def ensure_head_storage_generator_membership(context: PluginContext) -> None: # Fallback: For all generators and storages with matching _head names, ensure membership exists for gen_name, gen in generators_by_name.items(): - if gen_name not in pumped_storage_target_names: - continue if gen_name.endswith("_head"): storage = storages_by_name.get(gen_name) if storage is not None: @@ -470,7 +429,6 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: from r2x_sienna_to_plexos.getters import _build_generator_display_name_index display_name_index = _build_generator_display_name_index(context) - pumped_storage_target_names = _build_pumped_storage_target_generator_name_index(context) generators_by_name = {g.name: g for g in _target_system(context).get_components(PLEXOSGenerator)} storages_by_name = {s.name: s for s in _target_system(context).get_components(PLEXOSStorage)} @@ -522,8 +480,6 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: if target_gen is None: logger.debug("No PLEXOSGenerator found for HydroTurbine '{}', skipping.", tname) continue - if target_gen.name not in pumped_storage_target_names: - continue _ensure_membership(context, target_gen, target_storage, CollectionEnum.TailStorage) total_memberships += 1 @@ -536,8 +492,6 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: target_gen = generators_by_name.get(target_gen_name) if target_gen is None: continue - if target_gen.name not in pumped_storage_target_names: - continue for reservoir in getattr(turbine, "reservoirs", None) or []: location = getattr(getattr(reservoir, "reservoir_location", None), "value", None) @@ -556,8 +510,6 @@ def ensure_tail_storage_generator_membership(context: PluginContext) -> None: # Fallback: For all generators and storages with matching _tail names, ensure membership exists for gen_name, gen in generators_by_name.items(): - if gen_name not in pumped_storage_target_names: - continue if gen_name.endswith("_tail"): storage = storages_by_name.get(gen_name) if storage is not None: diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py index 78445614..7a857131 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py @@ -24,7 +24,6 @@ ensure_reserve_time_series, ensure_tail_storage_generator_membership, ensure_transformer_node_memberships, - ensure_zone_node_memberships, ) @@ -61,7 +60,6 @@ def sienna_to_plexos(system: System, config: SiennaToPlexosConfig) -> System: ensure_generator_time_series(context) ensure_reserve_time_series(context) ensure_region_node_memberships(context) - ensure_zone_node_memberships(context) ensure_reference_node_memberships(context) ensure_generator_node_memberships(context) ensure_battery_node_memberships(context) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index 4b06edb6..aa4469b2 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -19,6 +19,7 @@ PLEXOSRegion, PLEXOSStorage, PLEXOSTransformer, + PLEXOSZone, ) from r2x_sienna.models import ( ACBus, @@ -1005,7 +1006,7 @@ def test_get_head_tail_storage_name(context, monkeypatch): assert getters.get_tail_storage_name(hydro, context).unwrap() == "hydro1_tail" -def test_get_head_tail_storage_name_err_without_pumped_storage_association(context): +def test_get_head_tail_storage_name_without_pumped_storage_association(context): hydro = HydroReservoir( name="hydro1", available=True, @@ -1024,8 +1025,8 @@ def test_get_head_tail_storage_name_err_without_pumped_storage_association(conte category="hydro_reservoir", ) - assert getters.get_head_storage_name(hydro, context).is_err() - assert getters.get_tail_storage_name(hydro, context).is_err() + assert getters.get_head_storage_name(hydro, context).unwrap() == "hydro1_head" + assert getters.get_tail_storage_name(hydro, context).unwrap() == "hydro1_tail" def test_reservoir_association_true_for_hydropumpturbine_links(context): @@ -1115,6 +1116,41 @@ def test_membership_component_child_node_battery(context): assert getters.membership_component_child_node(bat, context).unwrap().name == "N2" +def test_membership_node_child_zone_by_name(context): + area = Area(name="A1") + zone = LoadZone(name="Z1") + bus = ACBus(name="N1", area=area, load_zone=zone, number=1) + node = PLEXOSNode(name="N1") + target_zone = PLEXOSZone(name="Z1") + + context.source_system.add_component(area) + context.source_system.add_component(zone) + context.source_system.add_component(bus) + context.target_system.add_component(node) + context.target_system.add_component(target_zone) + + result = getters.membership_node_child_zone(node, context) + assert result.is_ok() + assert result.unwrap() == target_zone + + +def test_membership_node_child_zone_by_uuid(context): + area = Area(name="A1") + zone_like = types.SimpleNamespace(uuid="zone-uuid-1") + bus = ACBus(name="N1", area=area, load_zone=zone_like, number=1) + node = PLEXOSNode(name="N1") + target_zone = PLEXOSZone(name="Z_from_uuid", uuid="zone-uuid-1") + + context.source_system.add_component(area) + context.source_system.add_component(bus) + context.target_system.add_component(node) + context.target_system.add_component(target_zone) + + result = getters.membership_node_child_zone(node, context) + assert result.is_ok() + assert result.unwrap() == target_zone + + def test_membership_region_parent_node(context): region = PLEXOSRegion(name="A1") node = PLEXOSNode(name="A1") @@ -1744,6 +1780,111 @@ def test_get_tail_storage_name(context, monkeypatch): assert getters.get_tail_storage_name(hydro, context).unwrap() == "hydro1_tail" +def test_head_tail_storage_name_infers_location_from_suffix_when_missing(context): + head = HydroReservoir( + name="Plant_head", + available=True, + initial_level=500.0, + storage_level_limits={"min": 0.0, "max": 1000.0}, + spillage_limits=None, + inflow=0.0, + outflow=0.0, + level_targets=1000.0, + travel_time=0.0, + level_data_type="USABLE_VOLUME", + intake_elevation=0.0, + operation_cost=HydroReservoirCost.example(), + reservoir_location=None, + ext={"plant_name": "Plant"}, + ) + tail = HydroReservoir( + name="Plant_tail", + available=True, + initial_level=500.0, + storage_level_limits={"min": 0.0, "max": 1000.0}, + spillage_limits=None, + inflow=0.0, + outflow=0.0, + level_targets=1000.0, + travel_time=0.0, + level_data_type="USABLE_VOLUME", + intake_elevation=0.0, + operation_cost=HydroReservoirCost.example(), + reservoir_location=None, + ext={"plant_name": "Plant"}, + ) + + assert getters.get_head_storage_name(head, context).unwrap() == "Plant_head" + assert getters.get_tail_storage_name(head, context).is_err() + assert getters.get_head_storage_name(tail, context).is_err() + assert getters.get_tail_storage_name(tail, context).unwrap() == "Plant_tail" + + +def test_head_tail_storage_name_suffix_overrides_conflicting_metadata(context): + # Source metadata can be wrong; suffix should control head/tail assignment. + tail_with_wrong_metadata = HydroReservoir( + name="Abitibi Canyon_tail", + available=True, + initial_level=500.0, + storage_level_limits={"min": 0.0, "max": 1000.0}, + spillage_limits=None, + inflow=0.0, + outflow=0.0, + level_targets=1000.0, + travel_time=0.0, + level_data_type="USABLE_VOLUME", + intake_elevation=0.0, + operation_cost=HydroReservoirCost.example(), + reservoir_location=ReservoirLocation.HEAD, + ext={"plant_name": "Abitibi Canyon"}, + ) + + assert getters.get_head_storage_name(tail_with_wrong_metadata, context).is_err() + assert getters.get_tail_storage_name(tail_with_wrong_metadata, context).unwrap() == "Abitibi Canyon_tail" + + +def test_unsuffixed_reservoir_skips_side_with_explicit_reservoir(context): + explicit_head = HydroReservoir( + name="Wallace Dam_head", + available=True, + initial_level=500.0, + storage_level_limits={"min": 0.0, "max": 1000.0}, + spillage_limits=None, + inflow=0.0, + outflow=0.0, + level_targets=1000.0, + travel_time=0.0, + level_data_type="USABLE_VOLUME", + intake_elevation=0.0, + operation_cost=HydroReservoirCost.example(), + reservoir_location=None, + ext={"plant_name": "Wallace Dam"}, + ) + unsuffixed = HydroReservoir( + name="Wallace Dam", + available=True, + initial_level=500.0, + storage_level_limits={"min": 0.0, "max": 1000.0}, + spillage_limits=None, + inflow=0.0, + outflow=0.0, + level_targets=1000.0, + travel_time=0.0, + level_data_type="USABLE_VOLUME", + intake_elevation=0.0, + operation_cost=HydroReservoirCost.example(), + reservoir_location=None, + ext={"plant_name": "Wallace Dam"}, + ) + + context.source_system.add_component(explicit_head) + context.source_system.add_component(unsuffixed) + + assert getters.get_head_storage_name(explicit_head, context).unwrap() == "Wallace Dam_head" + assert getters.get_head_storage_name(unsuffixed, context).is_err() + assert getters.get_tail_storage_name(unsuffixed, context).unwrap() == "Wallace Dam_tail" + + def test_membership_reserve_child_generator_err(context): reserve = VariableReserve( name="missing", reserve_type=ReserveType.SPINNING, vors=10.0, direction="UP", requirement=100.0 diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index 0a2acfde..3f818738 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -20,14 +20,12 @@ PLEXOSReserve, PLEXOSStorage, PLEXOSTransformer, - PLEXOSZone, ) from r2x_sienna.models import ( ACBus, Arc, Area, EnergyReservoirStorage, - LoadZone, ThermalStandard, Transformer2W, VariableReserve, @@ -181,45 +179,6 @@ def test_ensure_region_node_memberships(context): assert any(m.collection == CollectionEnum.Region for m in memberships) -def test_ensure_zone_node_memberships(context): - area = Area(name="A1") - zone = LoadZone(name="Z1") - node = PLEXOSNode(name="N1") - target_zone = PLEXOSZone(name="Z1") - bus = ACBus(name="N1", area=area, load_zone=zone, number=1) - - context.source_system.add_component(area) - context.source_system.add_component(zone) - context.source_system.add_component(bus) - context.target_system.add_component(node) - context.target_system.add_component(target_zone) - - getters_utils.ensure_zone_node_memberships(context) - - memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) - assert any( - m.collection == CollectionEnum.Zone and m.parent_object == node and m.child_object == target_zone - for m in memberships - ) - - -def test_ensure_zone_node_memberships_skips_missing_target_zone(context): - area = Area(name="A1") - zone = LoadZone(name="Z1") - node = PLEXOSNode(name="N1") - bus = ACBus(name="N1", area=area, load_zone=zone, number=1) - - context.source_system.add_component(area) - context.source_system.add_component(zone) - context.source_system.add_component(bus) - context.target_system.add_component(node) - - getters_utils.ensure_zone_node_memberships(context) - - memberships = context.target_system.get_supplemental_attributes_with_component(node, PLEXOSMembership) - assert not any(m.collection == CollectionEnum.Zone for m in memberships) - - def test_ensure_transformer_node_memberships(context): node1 = PLEXOSNode(name="N1") node2 = PLEXOSNode(name="N2") @@ -901,6 +860,15 @@ def test_bus_name_to_area_and_zone_cache_and_non_area_object(context): assert mapping["B1"] == ("A1", "Z1") +def test_bus_name_to_area_and_zone_uses_zone_name_attribute(context): + zone_like = types.SimpleNamespace(name="Z2") + context.source_system.get_components = lambda _comp_type: [ + types.SimpleNamespace(name="B2", area="A2", load_zone=zone_like) + ] + mapping = getters_utils._bus_name_to_area_and_zone(context) + assert mapping["B2"] == ("A2", "Z2") + + def test_attach_reservoir_time_series_to_storage_paths(context): target_storage = PLEXOSStorage(name="Plant_head") @@ -970,8 +938,8 @@ def test_hydroturbine_driven_head_tail_memberships(context, monkeypatch): getters_utils.ensure_tail_storage_generator_membership(context) memberships = context.target_system.get_supplemental_attributes_with_component(gen, PLEXOSMembership) - assert not any(m.collection == CollectionEnum.HeadStorage for m in memberships) - assert not any(m.collection == CollectionEnum.TailStorage for m in memberships) + assert any(m.collection == CollectionEnum.HeadStorage for m in memberships) + assert any(m.collection == CollectionEnum.TailStorage for m in memberships) def test_generator_reserve_interface_and_battery_memberships(context, monkeypatch): diff --git a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py index 69cc3d34..a440945d 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py +++ b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py @@ -165,7 +165,7 @@ def test_sienna_storage_translates_to_plexos_storage(tmp_path): assert storage -def test_hydro_reservoir_without_pumped_storage_does_not_translate_to_plexos_storage(tmp_path): +def test_hydro_reservoir_without_suffix_translates_to_head_and_tail_storage(tmp_path): from infrasys.value_curves import LinearCurve from r2x_plexos.models import PLEXOSStorage from r2x_sienna.models import HydroReservoir @@ -201,9 +201,8 @@ def test_hydro_reservoir_without_pumped_storage_does_not_translate_to_plexos_sto assert result.total_rules > 0 storages = list(context.target_system.get_components(PLEXOSStorage)) - assert not any( - s.name in {"EI_head", "EI_tail", "EI_Reservoir_head", "EI_Reservoir_tail"} for s in storages - ) + assert any(s.name in {"EI_head", "EI_Reservoir_head"} for s in storages) + assert any(s.name in {"EI_tail", "EI_Reservoir_tail"} for s in storages) def test_sienna_interface_translates_to_plexos_interface(tmp_path): diff --git a/uv.lock b/uv.lock index 01d3e0f8..0594f171 100644 --- a/uv.lock +++ b/uv.lock @@ -27,6 +27,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "alabaster" version = "1.0.0" @@ -45,6 +57,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "authlib" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -54,6 +101,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -67,6 +132,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526, upload-time = "2026-04-20T19:02:23.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -76,6 +171,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -142,6 +285,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -217,6 +372,66 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/2c/fced34890f6e5a93a4b7afb2c71e8eee2a0719fb26193a0abf159ecb714d/cyclopts-4.10.2.tar.gz", hash = "sha256:d7b950457ef2563596d56331f80cbbbf86a2772535fb8b315c4f03bc7e6127f1", size = 166664, upload-time = "2026-04-08T23:57:45.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl", hash = "sha256:a1f2d6f8f7afac9456b48f75a40b36658778ddc9c6d406b520d017ae32c990fe", size = 204314, upload-time = "2026-04-08T23:57:46.969Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -226,6 +441,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -235,6 +468,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastmcp" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, +] + [[package]] name = "filelock" version = "3.20.1" @@ -296,6 +587,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "h5py" version = "3.15.1" @@ -344,6 +653,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/52/b8eea98dd5e9167ed9162a700cdb3040dfcf52e285bd33c548a6fb5f6a8c/h5py_stubs-0.1.2-py3-none-any.whl", hash = "sha256:22899b06f7cfe028ba8eabf9aebee79d6facd0aeaee965e65bc290df1360a5ca", size = 6383, upload-time = "2025-09-09T16:21:09.604Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -371,6 +717,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "infrasys" version = "1.0.0" @@ -401,6 +759,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -413,6 +816,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "librt" version = "0.7.4" @@ -531,6 +1014,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -552,6 +1060,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -679,6 +1196,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" }, +] + [[package]] name = "orjson" version = "3.11.5" @@ -782,6 +1324,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, ] +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -820,11 +1371,15 @@ name = "plexosdb" version = "1.3.4" source = { editable = "../plexosdb" } dependencies = [ + { name = "fastmcp" }, { name = "loguru" }, ] [package.metadata] -requires-dist = [{ name = "loguru" }] +requires-dist = [ + { name = "fastmcp", specifier = ">=3.0.0" }, + { name = "loguru" }, +] [package.metadata.requires-dev] dev = [ @@ -925,6 +1480,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + [[package]] name = "pyarrow" version = "22.0.0" @@ -961,6 +1541,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -976,6 +1565,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.41.5" @@ -1045,6 +1639,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1054,6 +1662,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -1095,6 +1726,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1104,6 +1753,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1412,6 +2086,20 @@ requires-dist = [ { name = "r2x-sienna", editable = "../r2x-sienna" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1440,6 +2128,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "roman-numerals" version = "4.1.0" @@ -1461,6 +2162,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + [[package]] name = "ruff" version = "0.6.9" @@ -1495,6 +2275,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/6a/606fc68d1d16725915832577bf0fe62621dbb0dcc931cd5b4bade72e9099/rust_ok-0.3.0-py3-none-any.whl", hash = "sha256:4b1635c0623c1a3a3f039a739fd47271bcdb4b5f4fc4fc833e70d197708d65c5", size = 7330, upload-time = "2026-02-03T21:07:05.696Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1628,6 +2421,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + [[package]] name = "taplo" version = "0.9.3" @@ -1730,6 +2549,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/68/35c1d87e608940badbcfeb630347aa0509897284684f61fab6423d02b253/uncalled_for-0.3.1.tar.gz", hash = "sha256:5e412ac6708f04b56bef5867b5dcf6690ebce4eb7316058d9c50787492bb4bca", size = 49693, upload-time = "2026-04-07T13:05:06.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl", hash = "sha256:074cdc92da8356278f93d0ded6f2a66dd883dbecaf9bc89437646ee2289cc200", size = 11361, upload-time = "2026-04-07T13:05:05.341Z" }, +] + [[package]] name = "urllib3" version = "2.6.2" @@ -1739,6 +2567,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] +[[package]] +name = "uvicorn" +version = "0.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/62b0d9a2cfc8b4de6771322dae30f2db76c66dae9ec32e94e176a44ad563/uvicorn-0.45.0.tar.gz", hash = "sha256:3fe650df136c5bd2b9b06efc5980636344a2fbb840e9ddd86437d53144fa335d", size = 87818, upload-time = "2026-04-21T10:43:46.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/88/d0f7512465b166a4e931ccf7e77792be60fb88466a43964c7566cbaff752/uvicorn-0.45.0-py3-none-any.whl", hash = "sha256:2db26f588131aeec7439de00f2dd52d5f210710c1f01e407a52c90b880d1fd4f", size = 69838, upload-time = "2026-04-21T10:43:45.029Z" }, +] + [[package]] name = "virtualenv" version = "20.35.4" @@ -1753,6 +2594,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "win32-setctime" version = "1.2.0" @@ -1761,3 +2707,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b66 wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] From b91ec4bb8cbd611fd16e6c7a2609da72e6073807 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Mon, 4 May 2026 18:30:57 -0600 Subject: [PATCH 14/16] feat: update pump hydro gens creation with reservoirs --- .../src/r2x_sienna_to_plexos/__init__.py | 2 + .../r2x_sienna_to_plexos/config/rules.json | 3 +- .../src/r2x_sienna_to_plexos/getters.py | 29 ++++++++ .../src/r2x_sienna_to_plexos/getters_utils.py | 73 +++++++++++++++++++ .../src/r2x_sienna_to_plexos/translation.py | 2 + .../tests/test_getters.py | 56 ++++++++++++++ .../tests/test_getters_utils.py | 44 +++++++++++ .../test_translation_rule_application.py | 2 + 8 files changed, 210 insertions(+), 1 deletion(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py index 5c36ead8..246d0117 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/__init__.py @@ -26,6 +26,7 @@ ensure_generator_node_memberships, ensure_head_storage_generator_membership, ensure_interface_line_memberships, + ensure_pumped_hydro_storages_created, ensure_reference_node_memberships, ensure_region_node_memberships, ensure_reserve_battery_memberships, @@ -61,6 +62,7 @@ "ensure_transformer_node_memberships", "ensure_head_storage_generator_membership", "ensure_tail_storage_generator_membership", + "ensure_pumped_hydro_storages_created", "membership_region_parent_node", "membership_region_child_node", "membership_reserve_child_generator", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json index a7de8aea..7bddce49 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/config/rules.json @@ -518,6 +518,7 @@ "name": "get_generator_name", "units": "get_dispatch_generator_units", "commit": "get_generator_commit", + "category": "get_pumped_hydro_category", "rating": "get_generator_rating", "max_ramp_up": "get_max_ramp_up", "max_ramp_down": "get_max_ramp_down", @@ -545,7 +546,7 @@ "name": "get_generator_name", "units": "get_dispatch_generator_units", "commit": "get_generator_commit", - "category": "get_generator_category", + "category": "get_pumped_hydro_category", "rating": "get_generator_rating", "max_ramp_up": "get_max_ramp_up", "max_ramp_down": "get_max_ramp_down", diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index cf164308..5d4f8a86 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -1334,6 +1334,35 @@ def get_generator_category(source_component: object, context: PluginContext) -> return Err(ValueError("Cannot resolve generator category; rule default will apply")) +@getter +def get_pumped_hydro_category( + source_component: HydroTurbine | HydroPumpTurbine, context: PluginContext +) -> Result[str, ValueError]: + """Resolve category for hydro turbines, demoting zero-pump-load units to ``hydro``. + + Sienna ``HydroTurbine``/``HydroPumpTurbine`` components default to a pumped + category, but units whose pump-load (derived from ``rating``) resolves to + zero are not actually pumped storage and should land in the regular + ``hydro`` category. When the pump load is non-zero we defer to the standard + category resolution chain so explicit overrides (e.g. ``gen_type_string``) + still apply, and otherwise let the rule default apply via ``Err``. + """ + rating = getattr(source_component, "rating", None) + pump_load_mw = 0.0 + if rating is not None: + magnitude = get_magnitude(rating) + if magnitude is not None: + pump_load_mw = abs(float(magnitude) * resolve_base_power(source_component)) + + if math.isclose(pump_load_mw, 0.0, abs_tol=1e-9): + return Ok("hydro") + + category = _resolve_generator_category(source_component, context) + if category is not None: + return Ok(category) + return Err(ValueError("Cannot resolve generator category; rule default will apply")) + + @getter def get_fuel_price( source_component: ThermalStandard | ThermalMultiStart, context: PluginContext diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py index decbfa32..821ce1af 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters_utils.py @@ -763,6 +763,79 @@ def ensure_interface_line_memberships(context: PluginContext) -> None: logger.info("Total {} Interface-Line memberships created.", total_memberships) +def ensure_pumped_hydro_storages_created(context: PluginContext) -> None: + """Synthesize head/tail PLEXOSStorage entries for pumped-hydro generators missing them. + + Pumped-hydro generators in PLEXOS need both a head and a tail storage so + the pump/generator pair can move energy between reservoirs and perform + arbitrage. When the source Sienna system has no reservoirs attached to a + pumped-hydro turbine (common for ReEDS-style aggregated systems), create + minimal ``PLEXOSStorage`` entries with ``units=1`` and ``max_volume`` / + ``initial_volume`` derived from the generator's ``max_capacity``, and + attach the corresponding ``HeadStorage`` / ``TailStorage`` memberships. + """ + target_system = _target_system(context) + storages_by_name = {s.name: s for s in target_system.get_components(PLEXOSStorage)} + + created_storages = 0 + created_memberships = 0 + for gen in target_system.get_components(PLEXOSGenerator): + if getattr(gen, "category", None) != "pumped-hydro": + continue + + memberships = target_system.get_supplemental_attributes_with_component(gen, PLEXOSMembership) + has_head = any( + m.collection == CollectionEnum.HeadStorage and m.parent_object == gen for m in memberships + ) + has_tail = any( + m.collection == CollectionEnum.TailStorage and m.parent_object == gen for m in memberships + ) + + if has_head and has_tail: + continue + + # ``max_capacity`` is in MW; PLEXOS storage volumes are in GWh. Size + # the synthesized reservoir for a typical pumped-hydro duration so the + # generator can run at full output for that many hours before the + # head storage empties (or the tail fills). Initial volume is half-full + # so the unit can both pump and generate immediately. + pumped_hydro_duration_hours = 10.0 + max_capacity_mw = float(getattr(gen, "max_capacity", 0.0) or 0.0) + if max_capacity_mw > 0.0: + max_volume = round(max_capacity_mw * pumped_hydro_duration_hours / 1000.0, 4) + else: + max_volume = 1.0 # GWh fallback for degenerate sources + initial_volume = round(max_volume * 0.5, 4) + + for suffix, collection, already_present in ( + ("_head", CollectionEnum.HeadStorage, has_head), + ("_tail", CollectionEnum.TailStorage, has_tail), + ): + if already_present: + continue + storage_name = f"{gen.name}{suffix}" + storage = storages_by_name.get(storage_name) + if storage is None: + storage = PLEXOSStorage( + name=storage_name, + category="pumped-hydro", + units=1, + max_volume=max_volume, + initial_volume=initial_volume, + ) + target_system.add_component(storage) + storages_by_name[storage_name] = storage + created_storages += 1 + _ensure_membership(context, gen, storage, collection) + created_memberships += 1 + + logger.info( + "Synthesized {} pumped-hydro storages and {} memberships for generators missing reservoirs.", + created_storages, + created_memberships, + ) + + def ensure_pumped_hydro_storage_memberships(context: PluginContext) -> None: """Create Generator->Storage memberships for pumped hydro generators.""" storages_by_name = {s.name: s for s in _target_system(context).get_components(PLEXOSStorage)} diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py index 7a857131..053b0cb1 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/translation.py @@ -17,6 +17,7 @@ ensure_generator_time_series, ensure_head_storage_generator_membership, ensure_interface_line_memberships, + ensure_pumped_hydro_storages_created, ensure_reference_node_memberships, ensure_region_node_memberships, ensure_reserve_battery_memberships, @@ -69,5 +70,6 @@ def sienna_to_plexos(system: System, config: SiennaToPlexosConfig) -> System: ensure_interface_line_memberships(context) ensure_head_storage_generator_membership(context) ensure_tail_storage_generator_membership(context) + ensure_pumped_hydro_storages_created(context) return context.target_system diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index aa4469b2..01454c6e 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -842,6 +842,62 @@ def test_get_turbine_pump_load_and_efficiency(context): assert getters.get_turbine_pump_efficiency(ht, context).unwrap() == 92.0 +def test_get_pumped_hydro_category_demotes_zero_pump_load(context): + bus1 = ACBus(name="N2", base_voltage=115.0, number=1) + context.source_system.add_component(bus1) + ht_zero = HydroTurbine( + name="hydro-turbine-zero-pump", + available=True, + bus=bus1, + active_power=120.0, + reactive_power=0.0, + rating=0.0, + active_power_limits=MinMax(min=15.0, max=150.0), + reactive_power_limits=MinMax(min=-45.0, max=45.0), + base_power=150.0, + operation_cost=HydroGenerationCost.example(), + powerhouse_elevation=350.0, + ramp_limits=UpDown(up=8.0, down=8.0), + time_limits=UpDown(up=1.5, down=1.5), + outflow_limits=MinMax(min=5.0, max=100.0), + efficiency=0.92, + turbine_type=HydroTurbineType.FRANCIS, + prime_mover_type=PrimeMoversType.OT, + conversion_factor=1.0, + reservoirs=[], + category="hydro_turbine", + ) + assert getters.get_pumped_hydro_category(ht_zero, context).unwrap() == "hydro" + + ht_pumped = HydroTurbine( + name="hydro-turbine-with-pump", + available=True, + bus=bus1, + active_power=120.0, + reactive_power=0.0, + rating=150.0, + active_power_limits=MinMax(min=15.0, max=150.0), + reactive_power_limits=MinMax(min=-45.0, max=45.0), + base_power=150.0, + operation_cost=HydroGenerationCost.example(), + powerhouse_elevation=350.0, + ramp_limits=UpDown(up=8.0, down=8.0), + time_limits=UpDown(up=1.5, down=1.5), + outflow_limits=MinMax(min=5.0, max=100.0), + efficiency=0.92, + turbine_type=HydroTurbineType.FRANCIS, + prime_mover_type=PrimeMoversType.OT, + conversion_factor=1.0, + reservoirs=[], + category="hydro_turbine", + ) + # Non-zero pump load: defer to standard resolution rather than demoting + # to "hydro". Either an explicit category resolves or rule default applies. + result = getters.get_pumped_hydro_category(ht_pumped, context) + if result.is_ok(): + assert result.unwrap() != "hydro" + + def test_get_thermal_forced_outage_rate_defaults(context): bus1 = ACBus(name="N2", base_voltage=115.0, number=1) context.source_system.add_component(bus1) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py index 3f818738..84fe6226 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters_utils.py @@ -380,6 +380,50 @@ def test_ensure_pumped_hydro_storage_memberships(context): assert any(m.collection == CollectionEnum.TailStorage for m in memberships_tail) +def test_ensure_pumped_hydro_storages_created_synthesizes_missing(context): + # Pumped-hydro generator with no head/tail storage attached. + gen = PLEXOSGenerator(name="ph_gen", category="pumped-hydro", max_capacity=200.0) + context.target_system.add_component(gen) + + # Hydro generator should be ignored entirely. + hydro_gen = PLEXOSGenerator(name="hydro_gen", category="hydro", max_capacity=50.0) + context.target_system.add_component(hydro_gen) + + getters_utils.ensure_pumped_hydro_storages_created(context) + + storages = {s.name: s for s in context.target_system.get_components(PLEXOSStorage)} + assert "ph_gen_head" in storages + assert "ph_gen_tail" in storages + assert storages["ph_gen_head"].units == 1 + # 200 MW * 10 h / 1000 = 2.0 GWh; initial volume is half-full. + assert storages["ph_gen_head"].max_volume == 2.0 + assert storages["ph_gen_head"].initial_volume == 1.0 + + memberships = context.target_system.get_supplemental_attributes_with_component(gen, PLEXOSMembership) + assert any(m.collection == CollectionEnum.HeadStorage for m in memberships) + assert any(m.collection == CollectionEnum.TailStorage for m in memberships) + + # Hydro generator gets nothing synthesized. + assert "hydro_gen_head" not in storages + assert "hydro_gen_tail" not in storages + + +def test_ensure_pumped_hydro_storages_created_skips_when_already_attached(context): + gen = PLEXOSGenerator(name="ph_gen", category="pumped-hydro", max_capacity=100.0) + head_storage = PLEXOSStorage(name="existing_head") + tail_storage = PLEXOSStorage(name="existing_tail") + context.target_system.add_component(gen) + context.target_system.add_component(head_storage) + context.target_system.add_component(tail_storage) + getters_utils._ensure_membership(context, gen, head_storage, CollectionEnum.HeadStorage) + getters_utils._ensure_membership(context, gen, tail_storage, CollectionEnum.TailStorage) + + getters_utils.ensure_pumped_hydro_storages_created(context) + + storages = {s.name for s in context.target_system.get_components(PLEXOSStorage)} + assert storages == {"existing_head", "existing_tail"} + + def test_ensure_generator_node_memberships(context): area = Area(name="A1") bus = ACBus(name="N1", area=area, number=1) diff --git a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py index a440945d..9c711f36 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py +++ b/packages/r2x-sienna-to-plexos/tests/test_translation_rule_application.py @@ -355,6 +355,7 @@ def _inner(_context): monkeypatch.setattr(translation_module, "ensure_interface_line_memberships", _mark("iface_line")) monkeypatch.setattr(translation_module, "ensure_head_storage_generator_membership", _mark("head")) monkeypatch.setattr(translation_module, "ensure_tail_storage_generator_membership", _mark("tail")) + monkeypatch.setattr(translation_module, "ensure_pumped_hydro_storages_created", _mark("ph_storage")) source = FakeSystem(name="source") result = translation_module.sienna_to_plexos(source, config=types.SimpleNamespace()) @@ -374,6 +375,7 @@ def _inner(_context): "iface_line", "head", "tail", + "ph_storage", ] From ccf1c2f67a009ff17e39beefeeaa477cee76c169 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 6 May 2026 18:05:14 -0600 Subject: [PATCH 15/16] test: update test with recent update for EI refactoring --- .../src/r2x_sienna_to_plexos/getters.py | 12 +- .../tests/test_getters.py | 158 ++---------------- .../tests/test_rules_loading.py | 19 --- 3 files changed, 19 insertions(+), 170 deletions(-) diff --git a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py index 5d4f8a86..be6e93e8 100644 --- a/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py +++ b/packages/r2x-sienna-to-plexos/src/r2x_sienna_to_plexos/getters.py @@ -108,7 +108,7 @@ def _get_reeds_thermal_category_from_fuel(source_component: Any, context: Plugin return None fuel_str = fuel.name if hasattr(fuel, "name") else str(fuel) - fuel_key = str(fuel_str).strip().upper() + fuel_key = str(fuel_str).strip().replace("-", "_").replace(" ", "_").upper() if not fuel_key: return None @@ -120,8 +120,14 @@ def _get_reeds_thermal_category_from_fuel(source_component: Any, context: Plugin for category, fuel_values in mapping.items(): if not isinstance(fuel_values, list): continue - if fuel_key in {str(value).strip().upper() for value in fuel_values}: - return str(category) + normalized_values = { + str(value).strip().replace("-", "_").replace(" ", "_").upper() for value in fuel_values + } + if fuel_key in normalized_values: + category_str = str(category).strip() + if category_str in {"natural-gas", "natural_gas", "gas"}: + return "gas-cc" + return category_str return None diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index 01454c6e..923f3ea2 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -5,7 +5,6 @@ import json import types from datetime import datetime, timedelta -from typing import ClassVar import pytest from infrasys.cost_curves import FuelCurve, UnitSystem @@ -159,50 +158,6 @@ def test_get_susceptance_transformers(tmp_path): assert getters.get_transformer_susceptance(t3, context).is_err() -def test_get_line_charging_susceptance_types(tmp_path): - context = make_context(tmp_path) - context.source_system = System(name="source") - context.target_system = System(name="target") - - bus1 = ACBus(name="N2", base_voltage=115.0, number=1) - bus2 = ACBus(name="N3", base_voltage=115.0, number=2) - bus3 = ACBus(name="N4", base_voltage=115.0, number=3) - bus4 = ACBus(name="N5", base_voltage=115.0, number=4) - context.source_system.add_component(bus1) - context.source_system.add_component(bus2) - context.source_system.add_component(bus3) - context.source_system.add_component(bus4) - - arc1 = Arc(from_to=bus1, to_from=bus2) - arc2 = Arc(from_to=bus3, to_from=bus4) - context.source_system.add_component(arc1) - context.source_system.add_component(arc2) - - l1_2 = Line( - name="line-1-2", - arc=arc1, - b=FromTo_ToFrom(from_to=3.0, to_from=3.0), - rating=100.0, - active_power_flow=100, - reactive_power_flow=100, - angle_limits=MinMax(min=-0.03, max=0.03), - ) - context.source_system.add_component(l1_2) - assert getters.get_line_charging_susceptance(l1_2, context).unwrap() == 3.0 - - l3_4 = Line( - name="line-3-4", - arc=arc2, - b=FromTo_ToFrom(from_to=7.0, to_from=7.0), - rating=100.0, - active_power_flow=100, - reactive_power_flow=100, - angle_limits=MinMax(min=-0.03, max=0.03), - ) - context.source_system.add_component(l3_4) - assert getters.get_line_charging_susceptance(l3_4, context).unwrap() == 7.0 - - def test_get_load_participation_factor(tmp_path): context = make_context(tmp_path) context.source_system = System(name="source") @@ -258,28 +213,6 @@ def test_get_line_min_flow_max_flow_with_rating(context): assert getters.get_line_max_flow(line, context).unwrap() == 10000.0 -def test_get_line_charging_susceptance_with_b(context): - bus1 = ACBus(name="N2", base_voltage=115.0, number=1) - bus3 = ACBus(name="N4", base_voltage=115.0, number=3) - context.source_system.add_component(bus1) - context.source_system.add_component(bus3) - - arc = Arc(from_to=bus1, to_from=bus3) - context.source_system.add_component(arc) - line = Line( - name="L1", - rating=100.0, - r=0.01, - x=0.1, - arc=arc, - b=FromTo_ToFrom(from_to=2.5, to_from=2.5), - active_power_flow=0.0, - reactive_power_flow=0.0, - angle_limits=MinMax(min=-0.03, max=0.03), - ) - assert getters.get_line_charging_susceptance(line, context).unwrap() == 2.5 - - def test_get_max_capacity_with_limits(context): gen = ThermalStandard( name="GEN1", @@ -805,7 +738,7 @@ def test_get_generator_category_maps_thermal_oil_fuel(context): def test_get_generator_category_thermal_prefers_fuel_over_prime_mover(context): gen = _make_thermal_generator_for_category_tests( - name="thermal-gas", + name="natural-gas", fuel=ThermalFuels.NATURAL_GAS, prime_mover_type=PrimeMoversType.ST, ) @@ -1191,13 +1124,15 @@ def test_membership_node_child_zone_by_name(context): def test_membership_node_child_zone_by_uuid(context): + zone_uuid = "11111111-1111-4111-8111-111111111111" area = Area(name="A1") - zone_like = types.SimpleNamespace(uuid="zone-uuid-1") - bus = ACBus(name="N1", area=area, load_zone=zone_like, number=1) + source_zone = LoadZone(name="source-zone-name", uuid=zone_uuid) + bus = ACBus(name="N1", area=area, load_zone=source_zone, number=1) node = PLEXOSNode(name="N1") - target_zone = PLEXOSZone(name="Z_from_uuid", uuid="zone-uuid-1") + target_zone = PLEXOSZone(name="Z_from_uuid", uuid=zone_uuid) context.source_system.add_component(area) + context.source_system.add_component(source_zone) context.source_system.add_component(bus) context.target_system.add_component(node) context.target_system.add_component(target_zone) @@ -1457,7 +1392,6 @@ def test_get_line_min_max_flow_and_charging_susceptance_none(context): ) assert getters.get_line_min_flow(line, context).unwrap() == -10000.0 assert getters.get_line_max_flow(line, context).unwrap() == 10000.0 - assert getters.get_line_charging_susceptance(line, context).unwrap() == 5.0 def test_get_power_or_standard_load_no_loads(context): @@ -1850,7 +1784,7 @@ def test_head_tail_storage_name_infers_location_from_suffix_when_missing(context level_data_type="USABLE_VOLUME", intake_elevation=0.0, operation_cost=HydroReservoirCost.example(), - reservoir_location=None, + reservoir_location=ReservoirLocation.HEAD, ext={"plant_name": "Plant"}, ) tail = HydroReservoir( @@ -1866,7 +1800,7 @@ def test_head_tail_storage_name_infers_location_from_suffix_when_missing(context level_data_type="USABLE_VOLUME", intake_elevation=0.0, operation_cost=HydroReservoirCost.example(), - reservoir_location=None, + reservoir_location=ReservoirLocation.TAIL, ext={"plant_name": "Plant"}, ) @@ -1913,7 +1847,7 @@ def test_unsuffixed_reservoir_skips_side_with_explicit_reservoir(context): level_data_type="USABLE_VOLUME", intake_elevation=0.0, operation_cost=HydroReservoirCost.example(), - reservoir_location=None, + reservoir_location=ReservoirLocation.HEAD, ext={"plant_name": "Wallace Dam"}, ) unsuffixed = HydroReservoir( @@ -1929,7 +1863,7 @@ def test_unsuffixed_reservoir_skips_side_with_explicit_reservoir(context): level_data_type="USABLE_VOLUME", intake_elevation=0.0, operation_cost=HydroReservoirCost.example(), - reservoir_location=None, + reservoir_location=ReservoirLocation.TAIL, ext={"plant_name": "Wallace Dam"}, ) @@ -2100,50 +2034,6 @@ def test_get_line_min_max_flow_none_rating(context): assert getters.get_line_max_flow(line, context).unwrap() == 99999.0 -def test_get_line_charging_susceptance_complex_b(context): - """Covers complex b branch in get_line_charging_susceptance.""" - bus1 = ACBus(name="N1", base_voltage=115.0, number=1) - bus2 = ACBus(name="N2", base_voltage=115.0, number=2) - context.source_system.add_component(bus1) - context.source_system.add_component(bus2) - arc = Arc(from_to=bus1, to_from=bus2) - context.source_system.add_component(arc) - line = Line( - name="L1", - arc=arc, - rating=100.0, - r=0.01, - x=0.1, - b=FromTo_ToFrom(from_to=4.5, to_from=4.5), - active_power_flow=0.0, - reactive_power_flow=0.0, - angle_limits=MinMax(min=-0.03, max=0.03), - ) - assert getters.get_line_charging_susceptance(line, context).unwrap() == 4.5 - - -def test_get_line_charging_susceptance_dict_b(context): - """Covers dict b branch in get_line_charging_susceptance.""" - bus1 = ACBus(name="N1", base_voltage=115.0, number=1) - bus2 = ACBus(name="N2", base_voltage=115.0, number=2) - context.source_system.add_component(bus1) - context.source_system.add_component(bus2) - arc = Arc(from_to=bus1, to_from=bus2) - context.source_system.add_component(arc) - line = Line( - name="L1", - arc=arc, - rating=100.0, - r=0.01, - x=0.1, - b={"from_to": 6.0, "to_from": 6.0}, - active_power_flow=0.0, - reactive_power_flow=0.0, - angle_limits=MinMax(min=-0.03, max=0.03), - ) - assert getters.get_line_charging_susceptance(line, context).unwrap() == 6.0 - - def test_get_max_capacity_zero_from_sienna(context): """Covers branch where sienna_get_max_active_power returns 0.0 and falls through to active_power_limits dict.""" @@ -2155,34 +2045,6 @@ class DummyWithLimits: assert getters.get_max_capacity(d, context).unwrap() == 55.0 -def test_get_max_capacity_uses_default_when_below_ten_mw(context): - expected = round(getters._get_defaults("gas-cc", "max_capacity_MW"), 2) - - class DummyFromRating: - rating = 1.1 - base_power = 1.0 - - class DummyFromLimits: - rating = None - active_power_limits = {"max": 9.5} # noqa: RUF012 - - assert getters.get_max_capacity(DummyFromRating(), context).unwrap() == expected - assert getters.get_max_capacity(DummyFromLimits(), context).unwrap() == expected - - -def test_get_max_capacity_below_ten_uses_generic_fallback_when_category_has_no_capacity_defaults(context): - expected_generic = round(getters._get_defaults("gas-cc", "max_capacity_MW"), 2) - - class DummyHydroLike: - # Maps to a category that does not define max_capacity_MW/capacity_MW defaults. - ext: ClassVar[dict[str, str]] = {"gen_type_string": "hydro"} - rating = 0.02 - base_power = 1.0 - - result = getters.get_max_capacity(DummyHydroLike(), context).unwrap() - assert result == expected_generic - - def test_get_component_rating_no_base_power(context): """Covers get_component_rating when rating is not None but base_power missing.""" diff --git a/packages/r2x-sienna-to-plexos/tests/test_rules_loading.py b/packages/r2x-sienna-to-plexos/tests/test_rules_loading.py index f71712bd..acd19106 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_rules_loading.py +++ b/packages/r2x-sienna-to-plexos/tests/test_rules_loading.py @@ -67,25 +67,6 @@ def test_has_storage_to_battery_rule() -> None: ), "Missing EnergyReservoirStorage -> PLEXOSBattery rule" -def test_synchronous_condenser_rule_defaults_to_units_zero() -> None: - """Verify SynchronousCondenser generators are exported deactivated for PLEXOS.""" - rules_path = files("r2x_sienna_to_plexos.config") / "rules.json" - rules_data = json.loads(rules_path.read_text()) - - syn_cond_rule = next( - ( - rule - for rule in rules_data - if rule.get("source_type") == "SynchronousCondenser" - and rule.get("target_type") == "PLEXOSGenerator" - ), - None, - ) - - assert syn_cond_rule is not None, "Missing SynchronousCondenser -> PLEXOSGenerator rule" - assert syn_cond_rule.get("defaults", {}).get("units") == 0 - - def test_rules_have_required_fields() -> None: """Verify all rules have essential structure.""" rules_path = files("r2x_sienna_to_plexos.config") / "rules.json" From 1931e5c31c0e5e7e307a6fe1211df99243e1bf4d Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Wed, 6 May 2026 18:07:50 -0600 Subject: [PATCH 16/16] test: increase testing coverage --- .../tests/test_getters.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/packages/r2x-sienna-to-plexos/tests/test_getters.py b/packages/r2x-sienna-to-plexos/tests/test_getters.py index 923f3ea2..fa184167 100644 --- a/packages/r2x-sienna-to-plexos/tests/test_getters.py +++ b/packages/r2x-sienna-to-plexos/tests/test_getters.py @@ -2778,3 +2778,100 @@ def test_attach_generator_time_series_uses_rating_when_limits_missing(tmp_path, getters._attach_generator_time_series(context, "GEN_RATING", PLEXOSGenerator(name="GEN_RATING")) assert list(attached[0].data) == [1.0, 2.0] + + +def test_resolve_generator_category_zonal2nodal_uses_reeds_defaults(monkeypatch, context): + comp = types.SimpleNamespace(name="zonal2nodal_gas-cc_cluster", ext={}) + monkeypatch.setattr( + getters, + "_get_defaults_data", + lambda _ctx: {"reeds_defaults": {"gas": {}, "gas-cc": {}, "wind-ons": {}}}, + ) + + assert getters._resolve_generator_category(comp, context) == "gas-cc" + + +def test_get_reeds_thermal_category_returns_none_for_non_list_mapping_values(monkeypatch, context): + gen = _make_thermal_generator_for_category_tests( + name="thermal-natgas", + fuel=ThermalFuels.NATURAL_GAS, + ) + monkeypatch.setattr( + getters, + "_get_defaults_data", + lambda _ctx: {"reeds_thermal_mapping": {"natural-gas": "NATURAL_GAS", "coal": ["COAL"]}}, + ) + + assert getters._get_reeds_thermal_category_from_fuel(gen, context) is None + + +def test_get_reservoir_location_helper_priority_order(): + by_name = types.SimpleNamespace(name="Plant_HEAD") + by_attr = types.SimpleNamespace(name="Plant", reservoir_location="tail") + by_ext = types.SimpleNamespace(name="Plant", ext={"RESERVOIR_LOCATION": "head"}) + unknown = types.SimpleNamespace(name="Plant") + + assert getters._get_reservoir_location(by_name) == "HEAD" + assert getters._get_reservoir_location(by_attr) == "TAIL" + assert getters._get_reservoir_location(by_ext) == "HEAD" + assert getters._get_reservoir_location(unknown) is None + + +def test_has_explicit_side_reservoir_for_base_detects_matching_side(monkeypatch, context): + current = types.SimpleNamespace(name="Plant", ext={"plant_name": "Plant"}, uuid="1") + explicit_head = types.SimpleNamespace(name="Plant_head", ext={"plant_name": "Plant"}, uuid="2") + other_plant = types.SimpleNamespace(name="Other_head", ext={"plant_name": "Other"}, uuid="3") + + fake_source = types.SimpleNamespace(get_components=lambda _cls: [current, explicit_head, other_plant]) + monkeypatch.setattr(getters, "_source_system", lambda _ctx: fake_source) + + assert getters._has_explicit_side_reservoir_for_base(current, context, side="HEAD") is True + assert getters._has_explicit_side_reservoir_for_base(current, context, side="TAIL") is False + + +def test_membership_component_child_node_err_when_source_generator_has_no_bus(context): + source_gen = _make_thermal_generator_for_category_tests( + name="gen-without-bus", + fuel=ThermalFuels.NATURAL_GAS, + ) + context.source_system.add_component(source_gen) + + result = getters.membership_component_child_node(PLEXOSGenerator(name="gen-without-bus"), context) + assert result.is_err() + assert "missing bus data" in str(result.err()) + + +def test_membership_interface_child_line_success_via_monkeypatched_index(monkeypatch, context): + target_line = PLEXOSLine(name="line-01") + context.target_system.add_component(target_line) + + source_interface = types.SimpleNamespace(name="IFACE-1", lines=[types.SimpleNamespace(name="line-01")]) + monkeypatch.setattr(getters, "_build_source_interface_name_index", lambda _ctx: {"IFACE-1": source_interface}) + + result = getters.membership_interface_child_line(types.SimpleNamespace(name="IFACE-1"), context) + assert result.is_ok() + assert result.unwrap() == target_line + + +def test_membership_line_parent_interface_success_and_missing_target(context): + from r2x_plexos.models import PLEXOSInterface + + source_interface = TransmissionInterface( + name="Interface-1", + active_power_flow_limits=MinMax(min=-100.0, max=100.0), + direction_mapping={"line-01": 1}, + ) + context.source_system.add_component(source_interface) + + line = PLEXOSLine(name="line-01") + + missing_target = getters.membership_line_parent_interface(line, context) + assert missing_target.is_err() + + target_interface = PLEXOSInterface(name="Interface-1") + context.target_system.add_component(target_interface) + context._cache.pop("target_interface_name_index", None) + + result = getters.membership_line_parent_interface(line, context) + assert result.is_ok() + assert result.unwrap().name == "Interface-1"