diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ece41a..5686c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] - 2026-10-07 + +### Added + +- Renewable Component Library (RCL) integration via `solarfarmer.rcl`: + `list_modules()`, `list_inverters()`, `download_file()`, and + `get_rate_limit_status()`. +- RCL catalog typing and generated wire models, including public filter/field + types and response item `TypedDict`s. +- `RCLRateLimitInfo` helper type with reset time and usage metrics. +- `PVSystem` integration methods: + `set_module_from_rcl()` and `set_inverter_from_rcl()` for catalog search, + download, and assignment of PAN/OND files. +- New RCL documentation and examples: + `docs/getting-started/rcl-component-library.md` and + `docs/notebooks/Example_RCL_Catalog.ipynb`. + +### Changed + +- SDK documentation and navigation updated to include RCL workflows and API + reference. + +### Fixed + +- Flush-mount `PVSystem` tilt handling now preserves `plant.tilt` in mounting + specifications instead of forcing `0.0`, aligning flush-mount behaviour with + user-provided fixed-tilt inputs. + ## [0.6.0] - 2026-09-24 ### Added diff --git a/pyproject.toml b/pyproject.toml index 62f3c05..574d2ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dnv-solarfarmer" -version = "0.6.0" +version = "0.7.0" description = "Python SDK for SolarFarmer, a bankable solar PV design and energy yield assessment tool by DNV." readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_construct_plant.py b/tests/test_construct_plant.py index f8aaf65..926ea78 100644 --- a/tests/test_construct_plant.py +++ b/tests/test_construct_plant.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import pytest @@ -152,52 +153,66 @@ def test_flush_mount_uses_plant_tilt(self): class TestSpecIdDerivation: """Verify spec IDs are derived via Path.stem (last-dot split), not split('.')[0].""" + @staticmethod + def _create_multi_dot_alias(source: "Path", target: "Path") -> None: + """Create a temp alias file with a multi-dot name for stem extraction tests.""" + try: + target.symlink_to(source) + return + except OSError: + pass + + try: + target.hardlink_to(source) + return + except OSError: + pass + + # Last-resort fallback for platforms without link privileges. + target.write_bytes(source.read_bytes()) + def test_multi_dot_pan_filename_produces_correct_spec_id(self, bern_2d_racks_inputs): """PAN filenames with multiple dots must use Path.stem for spec ID.""" - from pathlib import Path - from solarfarmer.models.pvsystem.pvsystem import get_module_info_from_pan p = PVSystem(latitude=46.95, longitude=7.44) original = Path(bern_2d_racks_inputs) / "CanadianSolar_CS6U-330M_APP.PAN" multi_dot = Path(bern_2d_racks_inputs) / "Trina_TSM-DEG19C.20-550_APP.PAN" - created_link = False + created_temp_file = False try: if not multi_dot.exists(): - multi_dot.symlink_to(original) - created_link = True + self._create_multi_dot_alias(original, multi_dot) + created_temp_file = True p.pan_files = {"TestModule": str(multi_dot)} info = get_module_info_from_pan(p) # Path.stem gives "Trina_TSM-DEG19C.20-550_APP" # split(".")[0] would give "Trina_TSM-DEG19C" — wrong assert info["pan_filename"] == "Trina_TSM-DEG19C.20-550_APP" finally: - if created_link and multi_dot.exists(): + if created_temp_file and multi_dot.exists(): multi_dot.unlink() def test_multi_dot_ond_filename_produces_correct_spec_id(self, bern_2d_racks_inputs): """OND filenames with multiple dots must use Path.stem for spec ID.""" - from pathlib import Path - from solarfarmer.models.pvsystem.pvsystem import get_inverter_info_from_ond p = PVSystem(latitude=46.95, longitude=7.44) original = Path(bern_2d_racks_inputs) / "Sungrow_SG125HV_APP.OND" multi_dot = Path(bern_2d_racks_inputs) / "SMA_Sunny-Central.2200-UP.OND" - created_link = False + created_temp_file = False try: if not multi_dot.exists(): - multi_dot.symlink_to(original) - created_link = True + self._create_multi_dot_alias(original, multi_dot) + created_temp_file = True p.ond_files = {"TestInverter": str(multi_dot)} info = get_inverter_info_from_ond(p) # Path.stem gives "SMA_Sunny-Central.2200-UP" # split(".")[0] would give "SMA_Sunny-Central" — wrong assert info["ond_filename"] == "SMA_Sunny-Central.2200-UP" finally: - if created_link and multi_dot.exists(): + if created_temp_file and multi_dot.exists(): multi_dot.unlink() diff --git a/tests/test_endpoint_about.py b/tests/test_endpoint_about.py index cebc217..cf68711 100644 --- a/tests/test_endpoint_about.py +++ b/tests/test_endpoint_about.py @@ -30,7 +30,7 @@ def test_about_version_structure(self, api_key): assert len(result["solarFarmerCoreVersion"]) > 0 assert len(result["solarFarmerApiVersion"]) > 0 - @pytest.mark.parametrize("version,expected_version_prefix", [("v5", "5."), ("v6", "6.")]) + @pytest.mark.parametrize("version,expected_version_prefix", [("v6", "6."), ("v7", "7.")]) def test_about_all_versions_return_proper_structure( self, api_key, version, expected_version_prefix ): @@ -88,14 +88,14 @@ def test_failure_returns_empty_dict(self, mock_client_cls): @patch("solarfarmer.endpoint_about.Client") def test_version_forwarded_to_build_api_url(self, mock_client_cls, mock_build_url): """The version argument is forwarded to build_api_url.""" - mock_build_url.return_value = "http://mock/v5" + mock_build_url.return_value = "http://mock/v7" mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.get.return_value = self._ok_response() - solarfarmer.about(version="v5", api_key="test_key") + solarfarmer.about(version="v7", api_key="test_key") - mock_build_url.assert_called_once_with("v5") + mock_build_url.assert_called_once_with("v7") @patch("solarfarmer.endpoint_about.Client") def test_malformed_success_response_logs_error_and_returns_data(self, mock_client_cls, caplog):