nwp500-python 9.3.1__tar.gz → 9.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/workflows/ci.yml +33 -3
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/CHANGELOG.rst +184 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/Makefile +4 -3
- {nwp500_python-9.3.1/src/nwp500_python.egg-info → nwp500_python-9.4.0}/PKG-INFO +1 -1
- nwp500_python-9.4.0/RELEASE.md +418 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/index.rst +1 -0
- nwp500_python-9.4.0/docs/explanation/tou-recovery-cap.rst +188 -0
- nwp500_python-9.4.0/docs/how-to/maintenance.rst +139 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/schedule-operation.rst +36 -69
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/track-energy.rst +43 -15
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/openapi.yaml +15 -29
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/data_conversions.rst +2 -2
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/device_status.rst +2 -2
- nwp500_python-9.4.0/docs/reference/protocol/mqtt_protocol.rst +1230 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/quick_reference.rst +61 -29
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/cli.rst +133 -24
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/events.rst +3 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/models.rst +101 -62
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/mqtt_client.rst +174 -66
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/error_code_demo.py +3 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/bump_version.py +4 -5
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/__init__.py +12 -4
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/__main__.py +205 -16
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/handlers.py +283 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/output_formatters.py +62 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/presentation.py +251 -8
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/rich_output.py +55 -10
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/converters.py +1 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/enums.py +40 -16
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/__init__.py +13 -4
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/device.py +8 -5
- nwp500_python-9.4.0/src/nwp500/models/diagnostics.py +147 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/energy.py +35 -3
- nwp500_python-9.4.0/src/nwp500/models/firmware.py +47 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/schedule.py +105 -157
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/status.py +13 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/_control_commands.py +42 -33
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/_device_subscriptions.py +105 -17
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/client.py +76 -3
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/control.py +435 -157
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/subscriptions.py +327 -157
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/utils.py +9 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/reservations.py +3 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/topic_builder.py +49 -6
- {nwp500_python-9.3.1 → nwp500_python-9.4.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/SOURCES.txt +5 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/scm_file_list.json +6 -1
- nwp500_python-9.4.0/src/nwp500_python.egg-info/scm_version.json +8 -0
- nwp500_python-9.4.0/tests/test_app_queries.py +901 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_commands.py +434 -1
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_rest_models.py +19 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_protocol_correctness.py +353 -132
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_public_api.py +59 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_reservations.py +21 -0
- nwp500_python-9.4.0/uv.lock +3 -0
- nwp500_python-9.3.1/RELEASE.md +0 -393
- nwp500_python-9.3.1/docs/how-to/maintenance.rst +0 -119
- nwp500_python-9.3.1/docs/reference/protocol/mqtt_protocol.rst +0 -929
- nwp500_python-9.3.1/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.coveragerc +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.gitignore +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/LICENSE.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/README.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/Makefile +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/conf.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/index.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/README.md +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/mask.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/pyproject.toml +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/README.md +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/format.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/lint.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/setup.cfg +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/setup.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/conftest.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_events.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_models.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tox.ini +0 -0
|
@@ -69,15 +69,45 @@ jobs:
|
|
|
69
69
|
|
|
70
70
|
- name: Run tox tests
|
|
71
71
|
run: tox -e default
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
test-without-cli-extras:
|
|
74
|
+
# tox installs the cli extra, so the job above never exercises an
|
|
75
|
+
# install without click or rich. release.yml installs
|
|
76
|
+
# -e ".[testing]", which has neither, and a test module importing
|
|
77
|
+
# click above its ImportError guard failed there and nowhere else,
|
|
78
|
+
# after the release tag was already pushed. This job runs the
|
|
79
|
+
# release workflow's install so that failure surfaces on the pull
|
|
80
|
+
# request instead.
|
|
81
|
+
name: Test without CLI extras
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
|
|
87
|
+
- name: Set up Python
|
|
88
|
+
uses: actions/setup-python@v5
|
|
89
|
+
with:
|
|
90
|
+
python-version: '3.14'
|
|
91
|
+
|
|
92
|
+
- name: Install project with testing extras only
|
|
73
93
|
run: |
|
|
74
94
|
python -m pip install --upgrade pip
|
|
75
|
-
python -m pip install
|
|
95
|
+
python -m pip install -e ".[testing]"
|
|
96
|
+
|
|
97
|
+
- name: Verify the CLI extras really are absent
|
|
98
|
+
run: |
|
|
99
|
+
if python -c 'import click' 2>/dev/null; then
|
|
100
|
+
echo "click is installed; this job no longer tests what it claims to"
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
- name: Run tests
|
|
105
|
+
run: python -m pytest
|
|
76
106
|
|
|
77
107
|
build:
|
|
78
108
|
name: Build Distribution
|
|
79
109
|
runs-on: ubuntu-latest
|
|
80
|
-
needs: [lint, test]
|
|
110
|
+
needs: [lint, test, test-without-cli-extras]
|
|
81
111
|
|
|
82
112
|
steps:
|
|
83
113
|
- uses: actions/checkout@v4
|
|
@@ -5,6 +5,189 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.4.0 (2026-09-12)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
This release realigns the MQTT command set with what the NaviLink app
|
|
12
|
+
actually sends. The app (version 2.03.00) was decompiled and its request
|
|
13
|
+
builder compared with the library; every query it sends now has a
|
|
14
|
+
method, six commands it never sends were removed, and the recirculation
|
|
15
|
+
schedule write was corrected. Breaking changes are listed under
|
|
16
|
+
*Removed* and *Changed*.
|
|
17
|
+
|
|
18
|
+
Added
|
|
19
|
+
-----
|
|
20
|
+
- **Installer diagnostics query.** ``request_diagnostics()`` /
|
|
21
|
+
``subscribe_diagnostics()`` and the ``nwp-cli diagnostics`` command
|
|
22
|
+
return the lifetime counters the app's installer screen shows: heat
|
|
23
|
+
pump and element energy, days since installation, fault event counts,
|
|
24
|
+
demand-response operation times, hot-water draw statistics and
|
|
25
|
+
component run times and start counts, as
|
|
26
|
+
:class:`~nwp500.models.DeviceDiagnostics`. The device answers the
|
|
27
|
+
``st/td/rd`` query with packed hex; the NaviLink cloud decodes it into
|
|
28
|
+
JSON only when the reply topic has the app's five-segment form, so the
|
|
29
|
+
library requests that form. Verified live: the energies and compressor
|
|
30
|
+
and element run times match the energy query's lifetime totals.
|
|
31
|
+
- **Monthly energy query.** ``request_energy_usage_monthly()`` /
|
|
32
|
+
``subscribe_energy_usage_monthly()`` and ``nwp-cli energy --years``
|
|
33
|
+
return one entry per year with twelve per-month items. Verified live.
|
|
34
|
+
- **Hourly energy query.** ``request_energy_usage_hourly()`` /
|
|
35
|
+
``subscribe_energy_usage_hourly()`` send the app's per-hour query. The
|
|
36
|
+
NWP500 tested never answered it in four attempts, so it has no CLI
|
|
37
|
+
command.
|
|
38
|
+
- **Recirculation schedule read.** ``request_recirculation_schedule()``
|
|
39
|
+
and ``nwp-cli recirc-schedule get``; the write is exposed as
|
|
40
|
+
``nwp-cli recirc-schedule set``, which checks the JSON before
|
|
41
|
+
connecting (known keys only, integers in range, at most 20 entries).
|
|
42
|
+
The read was verified live on a unit that reports recirculation
|
|
43
|
+
scheduling but has no pump (empty, disabled schedule); the write and
|
|
44
|
+
its echo are unverified. The CLI reports a write as updated only when
|
|
45
|
+
the echo matches what was written. ``RecirculationSchedule`` and
|
|
46
|
+
``RecirculationScheduleEntry`` fields are strict integers, so ``True``
|
|
47
|
+
or ``6.0`` is rejected instead of coerced.
|
|
48
|
+
- **Firmware download info.** ``request_firmware_download_info()`` /
|
|
49
|
+
``subscribe_firmware_download_info()`` and ``nwp-cli firmware info``
|
|
50
|
+
return :class:`~nwp500.models.FirmwareDownloadInfo`. The device replies
|
|
51
|
+
on its own topic rather than a client-keyed one. Verified live.
|
|
52
|
+
- **Session end.** ``end_session()`` sends the ``st/end`` query the app
|
|
53
|
+
sends whenever it leaves a device. No reply to it has been observed.
|
|
54
|
+
Unlike other commands it is never queued while disconnected (it raises
|
|
55
|
+
``MqttNotConnectedError``), since a session end replayed after a
|
|
56
|
+
reconnect would end the new session.
|
|
57
|
+
- ``MqttConnectionConfig.send_session_end_on_disconnect`` (default
|
|
58
|
+
``True``) controls whether ``disconnect()`` sends it.
|
|
59
|
+
- ``subscribe_firmware_commit_response()`` receives the reply to
|
|
60
|
+
``commit_firmware_update()``, which is requested on the app-form
|
|
61
|
+
``res/commit-ota`` topic that no other subscription covers.
|
|
62
|
+
- **Controls with app builder cases that had no method:**
|
|
63
|
+
``set_vacation_duration(days)`` (``goout-day``; the app has a builder
|
|
64
|
+
case but no screen that calls it; verified live to change the day count
|
|
65
|
+
without changing the operation mode),
|
|
66
|
+
``set_air_filter_life(hours)`` (``air-filter-life``; the wire value is
|
|
67
|
+
hours divided by 500, matching the app's 0 or 1000-10000 picker;
|
|
68
|
+
verified live) and ``reset_condenser_fault()`` (``cond-fault-reset``,
|
|
69
|
+
installer-level in the app; acknowledged live, clearing effect not
|
|
70
|
+
observable without a fault). CLI: ``vacation-duration``,
|
|
71
|
+
``filter-life``, ``reset-condenser-fault``.
|
|
72
|
+
- **Command codes** for every member of the app's ``DeviceControlMGPP``
|
|
73
|
+
enum, with the declared-but-never-sent ones annotated.
|
|
74
|
+
- ``EnergyUsageResponse.type_of_usage``, ``get_year_data()``,
|
|
75
|
+
``MonthlyEnergyData.day`` and ``EnergyUsageDay.ep_usage`` /
|
|
76
|
+
``water_usage`` (declared by the app; not reported by the NWP500).
|
|
77
|
+
|
|
78
|
+
Changed
|
|
79
|
+
-------
|
|
80
|
+
- **Recirculation schedule models take the app's entry shape** (breaking).
|
|
81
|
+
:class:`~nwp500.models.RecirculationScheduleEntry` is now
|
|
82
|
+
``enable``/``week``/``hour``/``min``/``mode``/``param`` like a
|
|
83
|
+
reservation entry (``mode`` is the entry's pump on/off toggle in the
|
|
84
|
+
app's editor) and :class:`~nwp500.models.RecirculationSchedule` carries
|
|
85
|
+
``reservation_use`` plus ``reservation`` instead of ``schedule``. The
|
|
86
|
+
previous start/end window fields were not what the device accepts.
|
|
87
|
+
- ``MonthlyEnergyData.month`` is optional: monthly-query entries have no
|
|
88
|
+
month.
|
|
89
|
+
- **``NavienMqttClient.disconnect()`` now sends ``st/end``** to every
|
|
90
|
+
device that was passed to any subscription, as the NaviLink app does
|
|
91
|
+
when it leaves a device. The publishes go straight to the connection,
|
|
92
|
+
never into the offline command queue, stop if the connection drops,
|
|
93
|
+
are bounded by a short timeout and never fail the disconnect. What it
|
|
94
|
+
does to other clients connected to the same device (for example a Home
|
|
95
|
+
Assistant integration) is not known; turn it off with
|
|
96
|
+
``MqttConnectionConfig(send_session_end_on_disconnect=False)``.
|
|
97
|
+
- **CLI energy summaries total the requested periods.** ``energy
|
|
98
|
+
--month``, ``--months`` and ``--years`` printed the device's lifetime
|
|
99
|
+
total under a per-month or per-year heading. Each summary now covers
|
|
100
|
+
only the rows shown, and the lifetime total is printed once under its
|
|
101
|
+
own heading. (The response's ``total`` is lifetime whatever is
|
|
102
|
+
requested; checked live.)
|
|
103
|
+
- **Parameter validation.** ``set_air_filter_life()``,
|
|
104
|
+
``set_vacation_duration()`` and the daily, monthly and hourly energy
|
|
105
|
+
queries reject non-integers (including ``True`` and floats) and
|
|
106
|
+
out-of-range values; years must be 2000-2099 and ``--years`` drops
|
|
107
|
+
duplicates. ``filter-life``, ``--year`` and ``--years`` reject bad
|
|
108
|
+
input before connecting. ``request_energy_usage()`` now rejects an
|
|
109
|
+
empty month list.
|
|
110
|
+
|
|
111
|
+
Fixed
|
|
112
|
+
-----
|
|
113
|
+
- **``configure_recirculation_schedule()`` published to the wrong topic
|
|
114
|
+
with the wrong keys.** It sent ``{"schedule": [...]}`` on the bare
|
|
115
|
+
``ctrl`` topic; the app publishes RECIR_RESERVATION on
|
|
116
|
+
``ctrl/recirc-rsv/rd`` with ``reservationUse`` and ``reservation`` and
|
|
117
|
+
asks for the echo on ``recirc-rsv/rd``.
|
|
118
|
+
- **``commit_firmware_update()`` published to the bare ``ctrl`` topic.**
|
|
119
|
+
The app publishes OTA_COMMIT on ``ctrl/commit-ota`` and asks for the
|
|
120
|
+
reply on ``res/commit-ota``; the library now does the same. Not
|
|
121
|
+
exercised against a device.
|
|
122
|
+
- **Unsubscribing could remove the wrong handler.** When one callback was
|
|
123
|
+
registered for more than one device-wildcard subscription (status,
|
|
124
|
+
feature, firmware info), unsubscribing one removed whichever handler
|
|
125
|
+
came first. Handlers are now matched on subscription kind as well.
|
|
126
|
+
- **Query replies could reach another device's callback.** Client-keyed
|
|
127
|
+
reply topics are shared by every device a client queries, so with two
|
|
128
|
+
devices subscribed a reservation, energy, TOU, diagnostics or
|
|
129
|
+
recirculation reply reached both callbacks. Typed subscriptions now
|
|
130
|
+
ignore replies whose ``macAddress`` names a different device.
|
|
131
|
+
- Recirculation read-backs in hex carry the unused ``param`` as ``0xFF``
|
|
132
|
+
and in JSON as ``-1``; the model normalizes to ``-1`` so the two compare
|
|
133
|
+
equal.
|
|
134
|
+
- **Protocol documentation** rewritten from the decompiled request
|
|
135
|
+
builder and live captures: real topic structure, the anti-legionella
|
|
136
|
+
modes (``anti-leg-on [days]`` / ``anti-leg-off``, not
|
|
137
|
+
``anti-legionella-setting``), the message example that paired the
|
|
138
|
+
weekly-reservation code with ``dhw-temperature``, the undocumented
|
|
139
|
+
16777226 reservation write, response shapes for every query, and the
|
|
140
|
+
quick reference's invented ``header``/``body`` format with 0x11/0x21/0x31
|
|
141
|
+
codes that never existed. Stale ``docs/protocol/`` paths in code
|
|
142
|
+
comments and docstrings now point at ``docs/reference/protocol/``.
|
|
143
|
+
- ``models/schedule.py`` docstrings cited 33554444 (the hot button) for
|
|
144
|
+
the recirculation reservation; it is 33554440.
|
|
145
|
+
- CLI reference: token cache path is ``~/.nwp500_tokens.json``, ``-vv``
|
|
146
|
+
is a repeated ``-v`` (there was no ``--very-verbose``), and the
|
|
147
|
+
``--unit-system`` option and ``tou rates``/``plan``/``apply`` commands
|
|
148
|
+
are documented.
|
|
149
|
+
|
|
150
|
+
Removed
|
|
151
|
+
-------
|
|
152
|
+
- **Six MQTT commands the NaviLink app never sends** (breaking):
|
|
153
|
+
``check_firmware_update()``, ``reconnect_wifi()``, ``reset_wifi()``,
|
|
154
|
+
``set_freeze_protection_temperature()``, ``run_smart_diagnostic()`` and
|
|
155
|
+
``update_weekly_reservation()`` with
|
|
156
|
+
``subscribe_weekly_reservation_response()`` and the
|
|
157
|
+
``WeeklyReservationEntry`` / ``WeeklyReservationSchedule`` models. The
|
|
158
|
+
app's enum declares the first five codes but its request builder has
|
|
159
|
+
no case for them, so the library's ``ota-check``, ``wifi-reconnect``,
|
|
160
|
+
``wifi-reset``, ``frez-temp`` and ``smart-diagnostic`` mode strings were
|
|
161
|
+
invented. The app's weekly schedule is the ordinary reservation
|
|
162
|
+
program: it writes with 16777226 on ``ctrl/rsv/rd``
|
|
163
|
+
(``update_reservations()``), never with 33554438 or a ``rsv-weekly/rd``
|
|
164
|
+
topic. The ``CommandCode`` members stay, annotated as declared only.
|
|
165
|
+
|
|
166
|
+
Version 9.3.2 (2026-09-01)
|
|
167
|
+
==========================
|
|
168
|
+
|
|
169
|
+
Fixed
|
|
170
|
+
-----
|
|
171
|
+
- **A null ``errorCode`` no longer makes ``/device/list`` unparseable.**
|
|
172
|
+
The cloud returns ``"error": {"errorCode": null}`` on some devices.
|
|
173
|
+
:class:`~nwp500.models.DeviceErrorSummary` typed the field
|
|
174
|
+
``ErrorCode | int``, so validation of the whole listing failed and every
|
|
175
|
+
caller - including the Home Assistant integration, which could no longer
|
|
176
|
+
set up - lost the device entirely. ``error_code`` is now
|
|
177
|
+
``ErrorCode | int | None`` and defaults to ``None``: a null means the
|
|
178
|
+
cloud reported no code, which is not the same claim as ``NO_ERROR``.
|
|
179
|
+
Callers that treated the field as always-present should handle ``None``.
|
|
180
|
+
|
|
181
|
+
Security
|
|
182
|
+
--------
|
|
183
|
+
- **MAC address no longer logged in the clear when a reservations
|
|
184
|
+
unsubscribe fails.** ``fetch_reservations`` logged
|
|
185
|
+
``device_info.mac_address`` unredacted in its cleanup warning, while the
|
|
186
|
+
identical warning in ``update_reservations_confirmed`` a few lines below
|
|
187
|
+
passed it through ``redact_mac``. CodeQL flagged it as
|
|
188
|
+
``py/clear-text-logging-sensitive-data`` (high). Both call sites now
|
|
189
|
+
redact.
|
|
190
|
+
|
|
8
191
|
Version 9.3.1 (2026-08-29)
|
|
9
192
|
==========================
|
|
10
193
|
|
|
@@ -104,6 +287,7 @@ Fixed
|
|
|
104
287
|
other clients on the same device. Query results the device routes back to a
|
|
105
288
|
client-keyed topic, such as reservations and energy usage, were already
|
|
106
289
|
covered by the existing response wildcard.
|
|
290
|
+
|
|
107
291
|
Version 9.3.0 (2026-08-03)
|
|
108
292
|
==========================
|
|
109
293
|
|
|
@@ -53,10 +53,11 @@ build: clean ## Build distribution packages
|
|
|
53
53
|
check-release: lint format-check test validate-version ## Run all checks before release (lint, format check, tests, version validation)
|
|
54
54
|
@echo "✓ All checks passed! Ready for release."
|
|
55
55
|
|
|
56
|
-
release: check-release build ##
|
|
56
|
+
release: check-release build ## Run the release checks and build locally (does not publish)
|
|
57
57
|
@echo "✓ Release build complete!"
|
|
58
|
-
@echo "
|
|
59
|
-
@echo "To
|
|
58
|
+
@echo "This built locally and published nothing."
|
|
59
|
+
@echo "To release: make version-bump BUMP=patch, then push main and the tag."
|
|
60
|
+
@echo "Pushing the tag is what publishes to PyPI. See RELEASE.md."
|
|
60
61
|
|
|
61
62
|
publish-test: ## Publish to TestPyPI
|
|
62
63
|
python -m twine check dist/*
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Release Process
|
|
2
|
+
|
|
3
|
+
This document describes the release process for nwp500-python, including code quality checks, formatting, and publishing.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Install development dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
# or
|
|
12
|
+
make install-dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Release
|
|
16
|
+
|
|
17
|
+
Despite the name, this does not release anything - it runs the checks and
|
|
18
|
+
builds locally so you can see the release is in a fit state:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
make release
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will:
|
|
25
|
+
1. Run linting checks
|
|
26
|
+
2. Verify code formatting
|
|
27
|
+
3. Run all tests
|
|
28
|
+
4. Validate the version configuration
|
|
29
|
+
5. Clean build artifacts
|
|
30
|
+
6. Build distribution packages
|
|
31
|
+
|
|
32
|
+
Releasing is `make version-bump` followed by pushing the tag, which hands off
|
|
33
|
+
to CI. See [The Release Runs Itself](#5-the-release-runs-itself).
|
|
34
|
+
|
|
35
|
+
## Step-by-Step Release Process
|
|
36
|
+
|
|
37
|
+
### 1. Code Quality Checks
|
|
38
|
+
|
|
39
|
+
#### Format Code
|
|
40
|
+
|
|
41
|
+
Format all code with ruff:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
make format
|
|
45
|
+
# or
|
|
46
|
+
tox -e format
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This will:
|
|
50
|
+
- Automatically fix linting issues where possible
|
|
51
|
+
- Format code to comply with PEP 8 and project standards
|
|
52
|
+
- Sort imports according to isort rules
|
|
53
|
+
|
|
54
|
+
#### Check Linting
|
|
55
|
+
|
|
56
|
+
Check code without making changes:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
make lint
|
|
60
|
+
# or
|
|
61
|
+
tox -e lint
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Verify Formatting
|
|
65
|
+
|
|
66
|
+
Check that code is properly formatted:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
make format-check
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Run Tests
|
|
73
|
+
|
|
74
|
+
Run the test suite:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
make test
|
|
78
|
+
# or
|
|
79
|
+
pytest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run tests with coverage report:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
make test-cov
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Run All Checks
|
|
89
|
+
|
|
90
|
+
Run all quality checks at once:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
make check-release
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This runs:
|
|
97
|
+
- Linting checks
|
|
98
|
+
- Format verification
|
|
99
|
+
- Full test suite
|
|
100
|
+
|
|
101
|
+
### 4. Update Version and Changelog
|
|
102
|
+
|
|
103
|
+
#### Understanding Version Management
|
|
104
|
+
|
|
105
|
+
**IMPORTANT**: This project uses `setuptools_scm` to manage versions from git tags.
|
|
106
|
+
The version is **NOT** stored in any Python files or config files.
|
|
107
|
+
|
|
108
|
+
**DO NOT** edit the `version` field in `setup.cfg`'s `[pyscaffold]` section!
|
|
109
|
+
That field stores the PyScaffold tool version (4.6), not the package version.
|
|
110
|
+
|
|
111
|
+
#### Version Bump Process
|
|
112
|
+
|
|
113
|
+
The bump script owns the version heading, the changelog commit and the tag.
|
|
114
|
+
Do not write any of them by hand - `scripts/extract_changelog.py` parses the
|
|
115
|
+
heading it produces to build the GitHub release notes, and a hand-written
|
|
116
|
+
duplicate silently breaks that.
|
|
117
|
+
|
|
118
|
+
1. Make sure `CHANGELOG.rst` describes the release under `Unreleased`.
|
|
119
|
+
|
|
120
|
+
Add entries only - no `Version X.Y.Z` heading, no date. Normally each PR
|
|
121
|
+
has already added its own entry, so this is a review rather than a write:
|
|
122
|
+
|
|
123
|
+
```rst
|
|
124
|
+
Unreleased
|
|
125
|
+
==========
|
|
126
|
+
|
|
127
|
+
Fixed
|
|
128
|
+
-----
|
|
129
|
+
- **Something was broken.** It is no longer broken.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If you edit anything here, commit it before continuing: the script
|
|
133
|
+
refuses to run on a dirty working directory.
|
|
134
|
+
|
|
135
|
+
2. Run the bump script:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# For a patch release (X.Y.Z -> X.Y.Z+1)
|
|
139
|
+
make version-bump BUMP=patch
|
|
140
|
+
|
|
141
|
+
# For a minor release (X.Y.Z -> X.Y+1.0)
|
|
142
|
+
make version-bump BUMP=minor
|
|
143
|
+
|
|
144
|
+
# For a major release (X.Y.Z -> X+1.0.0)
|
|
145
|
+
make version-bump BUMP=major
|
|
146
|
+
|
|
147
|
+
# Or specify an explicit version
|
|
148
|
+
make version-bump BUMP=3.1.5
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The script will:
|
|
152
|
+
- Refuse to run unless the working directory is clean
|
|
153
|
+
- Refuse to run if the branch is behind its upstream (it warns, but
|
|
154
|
+
continues, if the branch is ahead or has no upstream)
|
|
155
|
+
- Get the current version from git tags
|
|
156
|
+
- Calculate the new version and validate the progression (prevents large
|
|
157
|
+
jumps)
|
|
158
|
+
- Insert `Version X.Y.Z (YYYY-MM-DD)` under `Unreleased` in `CHANGELOG.rst`
|
|
159
|
+
- Commit that as `Update changelog for vX.Y.Z`
|
|
160
|
+
- Create the annotated tag `vX.Y.Z`
|
|
161
|
+
|
|
162
|
+
3. Push the commit, then the tag:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git push origin main
|
|
166
|
+
git push origin vX.Y.Z
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Push the commit first. The tag triggers the release, and the release notes
|
|
170
|
+
come from the changelog at that commit.
|
|
171
|
+
|
|
172
|
+
#### Manual Version Tagging (Not Recommended)
|
|
173
|
+
|
|
174
|
+
If you need to create a tag manually:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git tag -a vX.Y.Z -m "Release version X.Y.Z"
|
|
178
|
+
git push origin vX.Y.Z
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Warning**: Manual tagging bypasses the clean-tree, not-behind-upstream and
|
|
182
|
+
version progression checks, and does not touch the changelog - so unless you
|
|
183
|
+
added the `Version X.Y.Z (YYYY-MM-DD)` heading yourself,
|
|
184
|
+
`scripts/extract_changelog.py` finds no matching section and the GitHub release
|
|
185
|
+
notes fall back to the bare line `Release X.Y.Z`. Use the version bump script
|
|
186
|
+
instead.
|
|
187
|
+
|
|
188
|
+
### 5. The Release Runs Itself
|
|
189
|
+
|
|
190
|
+
Pushing a `v*` tag starts `.github/workflows/release.yml`, which is the only
|
|
191
|
+
thing that publishes. In order, it:
|
|
192
|
+
|
|
193
|
+
1. **Pre-release Checks** - `ruff check`, `ruff format --check`, and `pytest`
|
|
194
|
+
against an install of `-e ".[testing]"`
|
|
195
|
+
2. **Build and Publish to PyPI** - `python -m build`, `twine check`, then
|
|
196
|
+
`pypa/gh-action-pypi-publish` via PyPI trusted publishing (no API tokens
|
|
197
|
+
are stored anywhere)
|
|
198
|
+
3. **Create GitHub Release** - release notes extracted from the `Version
|
|
199
|
+
X.Y.Z` section of `CHANGELOG.rst`
|
|
200
|
+
|
|
201
|
+
Each job depends on the one before it, so a failing check publishes nothing.
|
|
202
|
+
|
|
203
|
+
Watch it:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then confirm the result:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
gh release view vX.Y.Z
|
|
213
|
+
pip index versions nwp500-python
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Do Not Publish By Hand
|
|
217
|
+
|
|
218
|
+
`make publish` and `make publish-test` still exist as an emergency fallback,
|
|
219
|
+
but a normal release must not use them. The workflow has already uploaded the
|
|
220
|
+
artifacts by the time you could, and PyPI refuses to accept a version twice -
|
|
221
|
+
so running them after a successful release fails, and running them before it
|
|
222
|
+
makes the workflow fail instead. Publishing is CI's job.
|
|
223
|
+
|
|
224
|
+
#### If the Release Fails After You Pushed the Tag
|
|
225
|
+
|
|
226
|
+
The tag is the trigger, so a failing pre-release check leaves a tag on the
|
|
227
|
+
remote pointing at a commit that was never released. Nothing was published -
|
|
228
|
+
the publish job is gated behind the checks - so the version number is still
|
|
229
|
+
free. Fix the problem, then move the tag:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# after committing the fix and pushing main
|
|
233
|
+
git tag -d vX.Y.Z
|
|
234
|
+
git push origin :refs/tags/vX.Y.Z
|
|
235
|
+
git tag -a vX.Y.Z -m "Release version X.Y.Z"
|
|
236
|
+
git push origin vX.Y.Z
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Confirm first that nothing was published - if the publish job did succeed,
|
|
240
|
+
that version is gone for good and you need a new patch version instead:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
gh run view <run-id> --json jobs --jq '.jobs[] | "\(.name): \(.conclusion)"'
|
|
244
|
+
pip index versions nwp500-python
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 6. Building Locally (Optional)
|
|
248
|
+
|
|
249
|
+
Building locally is a sanity check, not part of publishing:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
make build
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This creates:
|
|
256
|
+
- `dist/nwp500_python-X.Y.Z.tar.gz` (source distribution)
|
|
257
|
+
- `dist/nwp500_python-X.Y.Z-py3-none-any.whl` (wheel)
|
|
258
|
+
|
|
259
|
+
The version comes from `git describe`, so building on an untagged commit
|
|
260
|
+
produces a development version like `9.3.1.dev3+g1a2b3c4` - that is expected,
|
|
261
|
+
and it is a quick way to confirm the tag you just created is what
|
|
262
|
+
`setuptools_scm` sees.
|
|
263
|
+
|
|
264
|
+
## Using Tox
|
|
265
|
+
|
|
266
|
+
You can also use tox directly for all steps:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Run lint checks
|
|
270
|
+
tox -e lint
|
|
271
|
+
|
|
272
|
+
# Format code
|
|
273
|
+
tox -e format
|
|
274
|
+
|
|
275
|
+
# Run tests
|
|
276
|
+
tox
|
|
277
|
+
|
|
278
|
+
# Build package
|
|
279
|
+
tox -e build
|
|
280
|
+
|
|
281
|
+
# Clean artifacts
|
|
282
|
+
tox -e clean
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Ruff Configuration
|
|
286
|
+
|
|
287
|
+
Ruff is configured in `pyproject.toml` with the following rules:
|
|
288
|
+
|
|
289
|
+
- **Line length**: 88 characters (Black-compatible)
|
|
290
|
+
- **Target version**: Python 3.7+
|
|
291
|
+
- **Enabled rules**:
|
|
292
|
+
- `E`, `W`: pycodestyle errors and warnings
|
|
293
|
+
- `F`: Pyflakes
|
|
294
|
+
- `I`: isort (import sorting)
|
|
295
|
+
- `UP`: pyupgrade (Python version upgrades)
|
|
296
|
+
- `B`: flake8-bugbear (common bugs)
|
|
297
|
+
- `C4`: flake8-comprehensions
|
|
298
|
+
- `SIM`: flake8-simplify
|
|
299
|
+
|
|
300
|
+
### Checking Specific Files
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Check specific file
|
|
304
|
+
ruff check src/nwp500/auth.py
|
|
305
|
+
|
|
306
|
+
# Format specific file
|
|
307
|
+
ruff format src/nwp500/auth.py
|
|
308
|
+
|
|
309
|
+
# Check and fix specific directory
|
|
310
|
+
ruff check --fix src/nwp500/
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Troubleshooting
|
|
314
|
+
|
|
315
|
+
### Linting Errors
|
|
316
|
+
|
|
317
|
+
If you encounter linting errors:
|
|
318
|
+
|
|
319
|
+
1. Try auto-fixing: `make format`
|
|
320
|
+
2. Review remaining errors: `make lint`
|
|
321
|
+
3. Manually fix any errors that can't be auto-fixed
|
|
322
|
+
4. Re-run checks: `make check-release`
|
|
323
|
+
|
|
324
|
+
### Test Failures
|
|
325
|
+
|
|
326
|
+
If tests fail:
|
|
327
|
+
|
|
328
|
+
1. Review the test output
|
|
329
|
+
2. Fix the issues in the code
|
|
330
|
+
3. Re-run tests: `make test`
|
|
331
|
+
4. Ensure all tests pass before release
|
|
332
|
+
|
|
333
|
+
### Build Errors
|
|
334
|
+
|
|
335
|
+
If build fails:
|
|
336
|
+
|
|
337
|
+
1. Clean build artifacts: `make clean`
|
|
338
|
+
2. Verify dependencies are installed: `pip install -e ".[dev]"`
|
|
339
|
+
3. Try building again: `make build`
|
|
340
|
+
|
|
341
|
+
## Pre-Release Checklist
|
|
342
|
+
|
|
343
|
+
Before releasing, ensure:
|
|
344
|
+
|
|
345
|
+
- [ ] Everything intended for the release is merged to `main`
|
|
346
|
+
- [ ] `CHANGELOG.rst` describes it all under `Unreleased`, with no
|
|
347
|
+
hand-written `Version X.Y.Z` heading
|
|
348
|
+
- [ ] All code is formatted: `make format`
|
|
349
|
+
- [ ] Linting passes: `make lint`
|
|
350
|
+
- [ ] All tests pass: `make test`
|
|
351
|
+
- [ ] Version configuration is valid: `make validate-version`
|
|
352
|
+
- [ ] Documentation is up to date
|
|
353
|
+
- [ ] Examples work correctly
|
|
354
|
+
- [ ] Working directory is clean and `main` is in sync with the remote
|
|
355
|
+
(the bump script enforces both)
|
|
356
|
+
- [ ] Build succeeds: `make build`
|
|
357
|
+
|
|
358
|
+
Then bump, push, and let the workflow publish - see
|
|
359
|
+
[The Release Runs Itself](#5-the-release-runs-itself).
|
|
360
|
+
|
|
361
|
+
## Publishing Credentials
|
|
362
|
+
|
|
363
|
+
There are none to set up. The release workflow publishes through
|
|
364
|
+
[PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/): PyPI is
|
|
365
|
+
configured to trust `release.yml` in this repository, and the job mints a
|
|
366
|
+
short-lived token through the `pypi` GitHub environment using its
|
|
367
|
+
`id-token: write` permission. No API token is stored in the repository, in
|
|
368
|
+
GitHub secrets, or on your machine.
|
|
369
|
+
|
|
370
|
+
`TWINE_USERNAME`/`TWINE_PASSWORD` and `~/.pypirc` are only relevant if you
|
|
371
|
+
ever need the emergency `make publish` fallback, which a normal release must
|
|
372
|
+
not use.
|
|
373
|
+
|
|
374
|
+
## Continuous Integration
|
|
375
|
+
|
|
376
|
+
Two workflows already cover this:
|
|
377
|
+
|
|
378
|
+
**`.github/workflows/ci.yml`** - on pushes and pull requests:
|
|
379
|
+
|
|
380
|
+
| Job | What it does |
|
|
381
|
+
|-----|--------------|
|
|
382
|
+
| Lint and Format Check | `tox -e lint` (`ruff check` and `ruff format --check`) |
|
|
383
|
+
| Security Check | `ruff check --select S src/` |
|
|
384
|
+
| Test on Python 3.14 | `tox -e default` - pytest and pyright, with the `cli` and `testing` extras |
|
|
385
|
+
| Test without CLI extras | pytest against an `-e ".[testing]"` install, matching what `release.yml` does |
|
|
386
|
+
| Build Distribution | `python -m build` plus `twine check` |
|
|
387
|
+
|
|
388
|
+
**`.github/workflows/release.yml`** - on a `v*` tag: pre-release checks, then
|
|
389
|
+
build and publish to PyPI, then create the GitHub release.
|
|
390
|
+
|
|
391
|
+
`Test without CLI extras` exists because `tox` installs the `cli` extra while
|
|
392
|
+
`release.yml` does not. Without it, a test module that imports `click` outside
|
|
393
|
+
its `ImportError` guard passes every pull request and fails only once the
|
|
394
|
+
release tag has been pushed.
|
|
395
|
+
|
|
396
|
+
## Quick Commands Reference
|
|
397
|
+
|
|
398
|
+
| Command | Description |
|
|
399
|
+
|---------|-------------|
|
|
400
|
+
| `make version-bump` | Bump version (requires BUMP=patch/minor/major/X.Y.Z) |
|
|
401
|
+
| `make help` | Show all available commands |
|
|
402
|
+
| `make install-dev` | Install with dev dependencies |
|
|
403
|
+
| `make format` | Format code with ruff |
|
|
404
|
+
| `make lint` | Check code with ruff |
|
|
405
|
+
| `make test` | Run tests |
|
|
406
|
+
| `make check-release` | Run all pre-release checks |
|
|
407
|
+
| `make release` | Full release build process |
|
|
408
|
+
| `make build` | Build distribution packages locally (CI builds what is published) |
|
|
409
|
+
| `make clean` | Remove build artifacts |
|
|
410
|
+
| `make publish-test` | Emergency fallback only - upload to TestPyPI by hand |
|
|
411
|
+
| `make publish` | Emergency fallback only - upload to PyPI by hand |
|
|
412
|
+
|
|
413
|
+
## More Information
|
|
414
|
+
|
|
415
|
+
- [Ruff Documentation](https://docs.astral.sh/ruff/)
|
|
416
|
+
- [Python Packaging User Guide](https://packaging.python.org/)
|
|
417
|
+
- [Twine Documentation](https://twine.readthedocs.io/)
|
|
418
|
+
- [Semantic Versioning](https://semver.org/)
|