diff --git a/.github/workflows/auto-dependabot.yaml b/.github/workflows/auto-dependabot.yaml index e87d5fb7a..d3f5aa144 100644 --- a/.github/workflows/auto-dependabot.yaml +++ b/.github/workflows/auto-dependabot.yaml @@ -1,21 +1,39 @@ name: Auto-merge Dependabot PR on: - pull_request: + # XXX: !!! SECURITY WARNING !!! + # pull_request_target has write access to the repo, and can read secrets. We + # need to audit any external actions executed in this workflow and make sure no + # checked out code is run (not even installing dependencies, as installing + # dependencies usually can execute pre/post-install scripts). We should also + # only use hashes to pick the action to execute (instead of tags or branches). + # For more details read: + # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + pull_request_target: permissions: - contents: write + contents: read pull-requests: write jobs: auto-merge: - if: github.actor == 'dependabot[bot]' + name: Auto-merge Dependabot PR + if: > + github.actor == 'dependabot[bot]' && + !contains(github.event.pull_request.title, 'the repo-config group') runs-on: ubuntu-slim steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }} + private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }} + - name: Auto-merge Dependabot PR - uses: frequenz-floss/dependabot-auto-approve@e943399cc9d76fbb6d7faae446cd57301d110165 # v1.5.0 + uses: frequenz-floss/dependabot-auto-approve@e943399cc9d76fbb6d7faae446cd57301d110165 # v1.5.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} dependency-type: 'all' auto-merge: 'true' merge-method: 'merge' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2547bbda..eb14cf2f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,11 +28,9 @@ jobs: strategy: fail-fast: false matrix: - arch: - - amd64 - - arm - os: + platform: - ubuntu-24.04 + - ubuntu-24.04-arm python: - "3.11" - "3.12" @@ -42,7 +40,7 @@ jobs: # that uses the same venv to run multiple linting sessions - "ci_checks_max" - "pytest_min" - runs-on: ${{ matrix.os }}${{ matrix.arch != 'amd64' && format('-{0}', matrix.arch) || '' }} + runs-on: ${{ matrix.platform }} steps: - name: Run nox @@ -61,7 +59,7 @@ jobs: needs: ["nox"] # We skip this job only if nox was also skipped if: always() && needs.nox.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim env: DEPS_RESULT: ${{ needs.nox.result }} steps: @@ -106,16 +104,14 @@ jobs: strategy: fail-fast: false matrix: - arch: - - amd64 - - arm - os: + platform: - ubuntu-24.04 + - ubuntu-24.04-arm python: - "3.11" - "3.12" - "3.13" - runs-on: ${{ matrix.os }}${{ matrix.arch != 'amd64' && format('-{0}', matrix.arch) || '' }} + runs-on: ${{ matrix.platform }} steps: - name: Setup Git @@ -163,7 +159,7 @@ jobs: needs: ["test-installation"] # We skip this job only if test-installation was also skipped if: always() && needs.test-installation.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim env: DEPS_RESULT: ${{ needs.test-installation.result }} steps: @@ -278,7 +274,7 @@ jobs: # discussions to create the release announcement in the discussion forums contents: write discussions: write - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim steps: - name: Download distribution files uses: actions/download-artifact@v8 diff --git a/.github/workflows/dco-merge-queue.yml b/.github/workflows/dco-merge-queue.yml index fb1cd90c7..d9597ad05 100644 --- a/.github/workflows/dco-merge-queue.yml +++ b/.github/workflows/dco-merge-queue.yml @@ -5,7 +5,7 @@ on: jobs: DCO: - runs-on: ubuntu-latest + runs-on: ubuntu-slim if: ${{ github.actor != 'dependabot[bot]' }} steps: - run: echo "This DCO job runs on merge_queue event and doesn't check PR contents" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8d02c1395..c327e7f24 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -7,7 +7,7 @@ jobs: permissions: contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Labeler # XXX: !!! SECURITY WARNING !!! diff --git a/.github/workflows/release-notes-check.yml b/.github/workflows/release-notes-check.yml index 55e6c09a2..3ed0af089 100644 --- a/.github/workflows/release-notes-check.yml +++ b/.github/workflows/release-notes-check.yml @@ -16,7 +16,7 @@ on: jobs: check-release-notes: name: Check release notes are updated - runs-on: ubuntu-latest + runs-on: ubuntu-slim permissions: pull-requests: read steps: diff --git a/.github/workflows/repo-config-migration.yaml b/.github/workflows/repo-config-migration.yaml index ac910e604..57a54c32d 100644 --- a/.github/workflows/repo-config-migration.yaml +++ b/.github/workflows/repo-config-migration.yaml @@ -47,7 +47,7 @@ jobs: app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }} private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }} - name: Migrate - uses: frequenz-floss/gh-action-dependabot-migrate@45994e185a9040449304a470e8f02d0e197873b4 # v1.1.1 + uses: frequenz-floss/gh-action-dependabot-migrate@07dc7e74726498c50726a80cc2167a04d896508f # v1.0.0 with: script-url-template: >- https://raw.githubusercontent.com/frequenz-floss/frequenz-repo-config-python/{version}/cookiecutter/migrate.py diff --git a/pyproject.toml b/pyproject.toml index 1a283f7c1..58fddce05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools == 80.10.2", "setuptools_scm[toml] == 9.2.2", - "frequenz-repo-config[lib] == 0.14.0", + "frequenz-repo-config[lib] == 0.17.0", ] build-backend = "setuptools.build_meta" @@ -13,12 +13,12 @@ build-backend = "setuptools.build_meta" name = "frequenz-sdk" description = "A development kit to interact with the Frequenz development platform" readme = "README.md" -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE"] keywords = ["frequenz", "python", "lib", "library", "sdk", "microgrid"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries", @@ -49,6 +49,7 @@ email = "floss@frequenz.com" [project.optional-dependencies] dev-flake8 = [ "flake8 == 7.3.0", + "flake8-datetimez == 20.10.0", "flake8-docstrings == 1.7.0", "flake8-pyproject == 1.2.4", # For reading the flake8 config from pyproject.toml "pydoclint == 0.8.3", @@ -65,7 +66,7 @@ dev-mkdocs = [ "mkdocs-material == 9.7.6", "mkdocstrings[python] == 1.0.4", "mkdocstrings-python == 2.0.3", - "frequenz-repo-config[lib] == 0.14.0", + "frequenz-repo-config[lib] == 0.17.0", ] dev-mypy = [ "mypy == 1.19.1", @@ -75,7 +76,7 @@ dev-mypy = [ # For checking the noxfile, docs/ script, and tests "frequenz-sdk[dev-mkdocs,dev-noxfile,dev-pytest]", ] -dev-noxfile = ["nox == 2026.2.9", "frequenz-repo-config[lib] == 0.14.0"] +dev-noxfile = ["nox == 2026.2.9", "frequenz-repo-config[lib] == 0.17.0"] dev-pylint = [ "pylint == 4.0.5", # For checking the noxfile, docs/ script, and tests @@ -83,7 +84,7 @@ dev-pylint = [ ] dev-pytest = [ "pytest == 9.0.3", - "frequenz-repo-config[extra-lint-examples] == 0.14.0", + "frequenz-repo-config[extra-lint-examples] == 0.17.0", "pytest-mock == 3.15.1", "pytest-asyncio == 1.3.0", "time-machine == 2.16.0", diff --git a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py index dbb4a16ec..8c2a90d81 100644 --- a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py +++ b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py @@ -193,7 +193,7 @@ async def test_setting_power( mocker: MockerFixture, ) -> None: """Test setting power.""" - traveller = time_machine.travel(datetime(2012, 12, 12)) + traveller = time_machine.travel(datetime(2012, 12, 12, tzinfo=timezone.utc)) mock_time = traveller.start() set_power = cast( diff --git a/tests/timeseries/_formulas/test_3_phases.py b/tests/timeseries/_formulas/test_3_phases.py index b6a89c364..151cbc5cd 100644 --- a/tests/timeseries/_formulas/test_3_phases.py +++ b/tests/timeseries/_formulas/test_3_phases.py @@ -7,7 +7,7 @@ import asyncio from collections import OrderedDict from collections.abc import Callable -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock import async_solipsism @@ -87,7 +87,7 @@ def stream_recv(comp_id: int) -> Receiver[Sample[Quantity]]: await asyncio.sleep(0.1) - now = datetime.now() + now = datetime.now(timezone.utc) for inputs, expected_output in io_pairs: _ = await asyncio.gather( *[ diff --git a/tests/timeseries/_formulas/test_general.py b/tests/timeseries/_formulas/test_general.py index 3076a9f65..f6e997380 100644 --- a/tests/timeseries/_formulas/test_general.py +++ b/tests/timeseries/_formulas/test_general.py @@ -7,7 +7,7 @@ import logging from collections import OrderedDict from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import NamedTuple from unittest.mock import AsyncMock, MagicMock @@ -67,7 +67,7 @@ def stream_recv(comp_id: ComponentId) -> Receiver[Sample[Quantity]]: async with formula as formula: results_rx = formula.new_receiver() await asyncio.sleep(0.1) # Allow time for setup - now = datetime.now() + now = datetime.now(timezone.utc) tests_passed = 0 for io_pair in io_pairs: @@ -400,7 +400,7 @@ def stream_recv(comp_id: int) -> Receiver[Sample[Quantity]]: result_chan = formula.new_receiver() await asyncio.sleep(0.1) - now = datetime.now() + now = datetime.now(timezone.utc) tests_passed = 0 for io_pair in io_pairs: io_input, io_output = io_pair @@ -779,7 +779,7 @@ def new_receiver(component_id: ComponentId) -> Receiver[Sample[Quantity]]: result_chan = formula.new_receiver() await asyncio.sleep(0.1) - now = datetime.now() + now = datetime.now(timezone.utc) async def send_sample(values: list[float | None]) -> None: nonlocal now diff --git a/tests/timeseries/_resampling/wall_clock_timer/test_config.py b/tests/timeseries/_resampling/wall_clock_timer/test_config.py index d38662c2b..e47790f36 100644 --- a/tests/timeseries/_resampling/wall_clock_timer/test_config.py +++ b/tests/timeseries/_resampling/wall_clock_timer/test_config.py @@ -96,7 +96,10 @@ def test_align_to_timezone_unaware() -> None: with pytest.raises( ValueError, match=r"^align_to (.*) should be a timezone aware datetime$" ): - _ = WallClockTimerConfig(align_to=datetime(2020, 1, 1, tzinfo=None)) + # Ignore the timezone-aware flake8 check because we want to validate it at runtime + _ = WallClockTimerConfig( + align_to=datetime(2020, 1, 1, tzinfo=None) # noqa: DTZ001 + ) _VALID_NUMBERS = [ diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index 8ae6b0920..bf50d882f 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -6,7 +6,7 @@ import asyncio import math -from datetime import datetime +from datetime import datetime, timezone from frequenz.channels import Broadcast, Receiver, Sender from frequenz.client.common.microgrid.components import ComponentId @@ -191,7 +191,7 @@ def power_3_phase_senders( self._meter_voltage_senders = voltage_senders(meter_ids) self._meter_power_3_phase_senders = power_3_phase_senders(meter_ids) - self._next_ts = datetime.now() + self._next_ts = datetime.now(timezone.utc) mocker.patch( "frequenz.sdk.microgrid._data_pipeline._DataPipeline" @@ -209,7 +209,7 @@ def power_3_phase_senders( def next_ts(self) -> None: """Increment the timestamp.""" - self._next_ts = datetime.now() + self._next_ts = datetime.now(timezone.utc) def _handle_task_done(self, task: asyncio.Task[None]) -> None: if task.cancelled(): diff --git a/tests/timeseries/test_base_types.py b/tests/timeseries/test_base_types.py index 7371f74e8..b291a284f 100644 --- a/tests/timeseries/test_base_types.py +++ b/tests/timeseries/test_base_types.py @@ -4,7 +4,7 @@ """Tests for timeseries base types.""" -from datetime import datetime +from datetime import datetime, timezone from frequenz.quantities import Power @@ -50,7 +50,7 @@ def test_bounds_contains_no_bounds() -> None: def test_system_bounds_contains() -> None: """Tests with complete system bounds.""" system_bounds = SystemBounds( - timestamp=datetime.now(), + timestamp=datetime.now(timezone.utc), inclusion_bounds=INCLUSION_BOUND, exclusion_bounds=EXCLUSION_BOUND, ) @@ -63,7 +63,7 @@ def test_system_bounds_contains() -> None: def test_system_bounds_contains_no_exclusion() -> None: """Tests with no exclusion bounds.""" system_bounds_no_exclusion = SystemBounds( - timestamp=datetime.now(), + timestamp=datetime.now(timezone.utc), inclusion_bounds=INCLUSION_BOUND, exclusion_bounds=None, ) @@ -74,7 +74,7 @@ def test_system_bounds_contains_no_exclusion() -> None: def test_system_bounds_contains_no_inclusion() -> None: """Tests with no inclusion bounds.""" system_bounds_no_inclusion = SystemBounds( - timestamp=datetime.now(), + timestamp=datetime.now(timezone.utc), inclusion_bounds=None, exclusion_bounds=EXCLUSION_BOUND, ) @@ -85,7 +85,7 @@ def test_system_bounds_contains_no_inclusion() -> None: def test_system_bounds_contains_no_bounds() -> None: """Tests with no bounds.""" system_bounds_no_bounds = SystemBounds( - timestamp=datetime.now(), + timestamp=datetime.now(timezone.utc), inclusion_bounds=None, exclusion_bounds=None, ) diff --git a/tests/timeseries/test_ringbuffer.py b/tests/timeseries/test_ringbuffer.py index f4667ea78..10ec96c71 100644 --- a/tests/timeseries/test_ringbuffer.py +++ b/tests/timeseries/test_ringbuffer.py @@ -433,12 +433,12 @@ def test_ringbuffer_empty_buffer() -> None: OrderedRingBuffer( empty_np_buffer, sampling_period=timedelta(seconds=1), - align_to=datetime(1, 1, 1), + align_to=datetime(1, 1, 1, tzinfo=timezone.utc), ) OrderedRingBuffer( empty_list_buffer, sampling_period=timedelta(seconds=1), - align_to=datetime(1, 1, 1), + align_to=datetime(1, 1, 1, tzinfo=timezone.utc), )