Skip to content

November release#1701

Merged
robbrad merged 44 commits into
masterfrom
November_release
Nov 8, 2025
Merged

November release#1701
robbrad merged 44 commits into
masterfrom
November_release

Conversation

@robbrad
Copy link
Copy Markdown
Owner

@robbrad robbrad commented Nov 8, 2025

Summary by CodeRabbit

  • New Features

    • Added support for Dumfries and Galloway Council
    • Newport lookup now accepts UPRN-based input
  • Bug Fixes

    • Updated service URLs for Brighton & Hove, Hounslow, and Norwich
    • Improved bin collection accuracy and reliability across multiple councils
    • Enhanced handling of multiple bin types per collection date
    • Adjusted Middlesbrough usage example to remove deprecated options
  • Documentation

    • Wiki updated with new council examples and corrected URLs

m26dvd and others added 30 commits October 21, 2025 22:41
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
fix: #1685 - New URL
fix: #1685 - New URL
fix: #1688 - BREAKING CHANGE
fix: #1382 - Removed the need for Selenium
Removing the requirement for Selenium and moving to UPRN
…ow.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…cil.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ouncil.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
James Cavill and others added 12 commits November 6, 2025 20:54
Docstrings generation was requested by @Jam3zs.

* #1678 (comment)

The following files were modified:

* `uk_bin_collection/uk_bin_collection/councils/TendringDistrictCouncil.py`
fix(tendring): read 'Next collection' column; harden cookie/iframe handling; normalise dd/MM/YYYY
fix: Council Fix Pack - November 2025
fix: Herefordshire Council
…ns/upload-artifact-5

chore: bump actions/upload-artifact from 4 to 5
Enhance HTTP requests with retry logic and headers
📝 Add docstrings to `fix/tendring-next-collection`
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 8, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a new Dumfries and Galloway council parser, updates test data and docs, and refactors many council parsers—moving several from HTML scraping to API flows, improving HTTP headers/retries, enhancing HTML parsing robustness, and introducing encrypted payload handling for Newport.

Changes

Cohort / File(s) Summary
Workflow
​.github/workflows/ha_compatibility_test.yml
Bumped GitHub Actions upload-artifact action from v4 to v5.
Test data & docs
uk_bin_collection/tests/input.json, wiki/Councils.md
Added Dumfries and Galloway; updated Brighton & Hove, Hounslow, Middlesbrough, Newport (UPRN), Norwich typo, Rochdale UPRN, and corresponding wiki examples/usage.
New council parser
uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py
New CouncilClass implementing ICS-based UPRN lookup and multi-bin-type parsing into {"bins": [...]}.
Encrypted API integration
uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py
Replaced scraping with AES‑CBC encrypted request/response flow; added NewportInput dataclass, encode_body/decode_response, and crypto constants.
API-based refactors
.../LondonBoroughHounslow.py, .../MiddlesbroughCouncil.py, .../RochdaleCouncil.py
Replaced Selenium/HTML parsing with session/auth API flows and JSON payload processing (token/session extraction, payload construction, JSON parsing).
HTML parsing & scraping hardening
.../HerefordshireCouncil.py, .../HartDistrictCouncil.py, .../TendringDistrictCouncil.py, .../WokinghamBoroughCouncil.py, .../LondonBoroughSutton.py
Robust DOM traversal, multi-bin expansion, improved date parsing (format fallbacks, year rollover), cookie/iframe handling, retry/backoff and sorting.
Request/header and webdriver improvements
.../BostonBoroughCouncil.py, .../BrightonandHoveCityCouncil.py, .../ChelmsfordCityCouncil.py, .../DerbyCityCouncil.py, .../LondonBoroughHarrow.py, .../SouthamptonCityCouncil.py, .../TendringDistrictCouncil.py
Added User-Agent/custom headers, timeouts, cookie banner handling, refined WebDriver init, and some hardcoded URL fixes.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Client/CLI
    participant Session as HTTP Session
    participant API as Council API
    participant Parser as Local Parser

    rect rgb(230,230,240)
    note over CLI,Parser: Legacy scraping (Selenium/BeautifulSoup)
    CLI->>Parser: launch webdriver / GET page
    Parser->>API: request HTML page
    API-->>Parser: HTML
    Parser->>Parser: parse DOM, extract bins
    Parser-->>CLI: return bins
    end

    rect rgb(220,245,220)
    note over CLI,Session: API-driven flow (Hounslow/Middlesbrough/Rochdale)
    CLI->>Session: POST auth / obtain token
    Session->>API: auth request
    API-->>Session: token/session
    Session->>API: POST data request (token + payload)
    API-->>Session: JSON response
    Session->>Parser: parse JSON -> bins
    Parser-->>CLI: return bins
    end

    rect rgb(245,230,230)
    note over CLI,Session: Encrypted transport (Newport)
    CLI->>CLI: AES-CBC encrypt payload (hex)
    CLI->>Session: POST encrypted hex
    Session->>API: forward encrypted request
    API-->>Session: encrypted hex response
    Session->>CLI: deliver hex
    CLI->>CLI: decrypt -> parse JSON -> bins
    CLI-->>Parser: return bins
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas needing extra attention:
    • NewportCityCouncil.py — crypto (AES‑CBC) implementation, padding, key/IV handling, and correct JSON mapping.
    • API auth/token flows — validate session/token extraction, error paths, and header usage (Hounslow, Rochdale, Middlesbrough).
    • Date parsing and year-rollover logic in Sutton/Tendring/Herefordshire — ensure edge cases and formats covered.
    • New DumfriesandGallowayCouncil parser — ICS parsing correctness and event splitting.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • dp247

Poem

🐰
I hopped through parsers, headers, and keys,
Turned messy HTML into tidy trees.
Dumfries joined the hop, Newport hid in code,
Tokens and retries cleared the old road.
May bins be found with every gentle thud.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'November release' is vague and generic, providing no meaningful information about the specific changes in the changeset. Use a more descriptive title that reflects the main changes, such as 'Update council integrations and add Dumfries and Galloway Council' or 'Improve scraper robustness and add new council support'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5015cf and 3f64192.

📒 Files selected for processing (15)
  • uk_bin_collection/uk_bin_collection/councils/BostonBoroughCouncil.py (4 hunks)
  • uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/DerbyCityCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/HartDistrictCouncil.py (3 hunks)
  • uk_bin_collection/uk_bin_collection/councils/HerefordshireCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py (3 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/WokinghamBoroughCouncil.py (4 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 8, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #1702

Docstrings generation was requested by @robbrad.

* #1701 (comment)

The following files were modified:

* `uk_bin_collection/uk_bin_collection/councils/BostonBoroughCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/DerbyCityCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/HartDistrictCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/HerefordshireCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py`
* `uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py`
* `uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py`
* `uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py`
* `uk_bin_collection/uk_bin_collection/councils/WokinghamBoroughCouncil.py`
@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 8, 2025

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
16 2 14 0
View the top 1 failed test(s) by shortest run time
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[DumfriesandGallowayCouncil]
Stack Traces | 0.445s run time
self = '<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/...replaceState(null, null, ogU);}}document.getElementsByTagName(\'head\')[0].appendChild(a);}());</script></body></html>'

    def parts(self):
        """Split the content line up into (name, parameters, values) parts.
        """
        try:
            st = escape_string(self)
            name_split = None
            value_split = None
            in_quotes = False
            for i, ch in enumerate(st):
                if not in_quotes:
                    if ch in ':;' and not name_split:
                        name_split = i
                    if ch == ':' and not value_split:
                        value_split = i
                if ch == '"':
                    in_quotes = not in_quotes
            name = unescape_string(st[:name_split])
            if not name:
                raise ValueError('Key name is required')
>           validate_token(name)

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12........./site-packages/icalendar/parser.py:326: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = '<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/...ots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><style>*{box-sizing'

    def validate_token(name):
        match = NAME.findall(name)
        if len(match) == 1 and name == match[0]:
            return
>       raise ValueError(name)
E       ValueError: <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><style>*{box-sizing

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12........./site-packages/icalendar/parser.py:115: ValueError

During handling of the above exception, another exception occurred:

fixturefunc = <function scrape_step at 0x7fbc82b22de0>
request = <FixtureRequest for <Function test_scenario_outline[DumfriesandGallowayCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fbc909994c0>, 'headless_mode': 'True', 'local_browser': 'False', 'selenium_url': 'http://localhost:4444'}

    def call_fixture_func(
        fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
    ) -> FixtureValue:
        if is_generator(fixturefunc):
            fixturefunc = cast(
                Callable[..., Generator[FixtureValue, None, None]], fixturefunc
            )
            generator = fixturefunc(**kwargs)
            try:
                fixture_result = next(generator)
            except StopIteration:
                raise ValueError(f"{request.fixturename} did not yield a value") from None
            finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
            request.addfinalizer(finalizer)
        else:
            fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
>           fixture_result = fixturefunc(**kwargs)

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/_pytest/fixtures.py:898: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../tests/step_defs/test_validate_council.py:101: in scrape_step
    context.parse_result = CollectData.run()
uk_bin_collection/uk_bin_collection/collect_data.py:101: in run
    return self.client_code(
uk_bin_collection/uk_bin_collection/collect_data.py:121: in client_code
    return get_bin_data_class.template_method(address_url, **kwargs)
uk_bin_collection/uk_bin_collection/get_bin_data.py:61: in template_method
    bin_data_dict = self.get_and_parse_data(this_url, **kwargs)
uk_bin_collection/uk_bin_collection/get_bin_data.py:84: in get_and_parse_data
    bin_data_dict = self.parse_data("", url=address_url, **kwargs)
.../uk_bin_collection/councils/DumfriesandGallowayCouncil.py:45: in parse_data
    upcoming_events = events(ics_url, start=now, end=future)
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/icalevents/icalevents.py:57: in events
    found_events += parse_events(
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/icalevents/icalparser.py:303: in parse_events
    calendar = Calendar.from_ical(content)
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12....../site-packages/icalendar/cal.py:1643: in from_ical
    comps = Component.from_ical(st, multiple=True)
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12....../site-packages/icalendar/cal.py:410: in from_ical
    name, params, vals = line.parts()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = '<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/...replaceState(null, null, ogU);}}document.getElementsByTagName(\'head\')[0].appendChild(a);}());</script></body></html>'

    def parts(self):
        """Split the content line up into (name, parameters, values) parts.
        """
        try:
            st = escape_string(self)
            name_split = None
            value_split = None
            in_quotes = False
            for i, ch in enumerate(st):
                if not in_quotes:
                    if ch in ':;' and not name_split:
                        name_split = i
                    if ch == ':' and not value_split:
                        value_split = i
                if ch == '"':
                    in_quotes = not in_quotes
            name = unescape_string(st[:name_split])
            if not name:
                raise ValueError('Key name is required')
            validate_token(name)
            if not value_split:
                value_split = i + 1
            if not name_split or name_split + 1 == value_split:
                raise ValueError('Invalid content line')
            params = Parameters.from_ical(st[name_split + 1: value_split],
                                          strict=self.strict)
            params = Parameters(
                (unescape_string(key), unescape_list_or_string(value))
                for key, value in iter(params.items())
            )
            values = unescape_string(st[value_split + 1:])
            return (name, params, values)
        except ValueError as exc:
>           raise ValueError(
                f"Content line could not be parsed into parts: '{self}': {exc}")
E           ValueError: Content line could not be parsed into parts: '<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><style>*{box-sizing:border-box;margin:0;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%;color:#313131;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}body{display:flex;flex-direction:column;height:100vh;min-height:100vh}.main-content{margin:8rem auto;padding-left:1.5rem;max-width:60rem}@media (width <= 720px){.main-content{margin-top:4rem}}.h2{line-height:2.25rem;font-size:1.5rem;font-weight:500}@media (width <= 720px){.h2{line-height:1.5rem;font-size:1.25rem}}#challenge-error-text{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iI0IyMEYwMyIgZD0iTTE2IDNhMTMgMTMgMCAxIDAgMTMgMTNBMTMuMDE1IDEzLjAxNSAwIDAgMCAxNiAzbTAgMjRhMTEgMTEgMCAxIDEgMTEtMTEgMTEuMDEgMTEuMDEgMCAwIDEtMTEgMTEiLz48cGF0aCBmaWxsPSIjQjIwRjAzIiBkPSJNMTcuMDM4IDE4LjYxNUgxNC44N0wxNC41NjMgOS41aDIuNzgzem0tMS4wODQgMS40MjdxLjY2IDAgMS4wNTcuMzg4LjQwNy4zODkuNDA3Ljk5NCAwIC41OTYtLjQwNy45ODQtLjM5Ny4zOS0xLjA1Ny4zODktLjY1IDAtMS4wNTYtLjM4OS0uMzk4LS4zODktLjM5OC0uOTg0IDAtLjU5Ny4zOTgtLjk4NS40MDYtLjM5NyAxLjA1Ni0uMzk3Ii8+PC9zdmc+");background-repeat:no-repeat;background-size:contain;padding-left:34px}@media (prefers-color-scheme: dark){body{background-color:#222;color:#d9d9d9}}</style><meta http-equiv="refresh" content="360"></head><body><div class="main-wrapper" role="main"><div class="main-content"><noscript><div class="h2"><span id="challenge-error-text">Enable JavaScript and cookies to continue</span></div></noscript></div></div><script>(function(){window._cf_chl_opt = {cvId: '3',cZone: 'www.dumfriesandgalloway.gov.uk',cType: 'managed',cRay: '99b3f2f0dd1fc94a',cH: 'FuhnMO4rhz6cauVxtrEgTrdTJtVxPzlWP7YiAYsBqBQ-1762593149-1.2.1.1-OBCds9Ao8wRj_u8A3aZp.uzIKr0GMtSIuEJ3iizias8lrw1LKuwZyhZlyt075kS7',cUPMDTk:"\/bins-recycling\/waste-collection-schedule\/download\/137034556?__cf_chl_tk=Hj50FuHlV5.4.DGhYZYKGGD.2wOtuPl5U52sgOF1F3o-1762593149-1.0.1.1-t_hNTTko5oGkAtpBm2JtaeElpdtNesJphYc4TFU.iZ0",cFPWv: 'g',cITimeS: '1762593149',cTplC:0,cTplV:5,cTplB: '0',fa:"\/bins-recycling\/waste-collection-schedule\/download\/137034556?__cf_chl_f_tk=Hj50FuHlV5.4.DGhYZYKGGD.2wOtuPl5U52sgOF1F3o-1762593149-1.0.1.1-t_hNTTko5oGkAtpBm2JtaeElpdtNesJphYc4TFU.iZ0",md: 'XZ0gDrnLq7TWZswI5Yys2NkXrmtQZ4Q7ixgxRXuNmiQ-1762593149-1.2.1.1-5XTSNvVniHXH7FvTTo_gGPXEwy8OOtfDKsrSPgWePGAFAScwUnhTnkLbTn6o4In8NWA9S4oyW8WmZIInmJq69gmTHxjRng8aldlcppR.zjuISRp1.yLffIPAqKG7M8CZYMKrRjY6CVWqX08P80CRhVmOY1bRIS5376DanaaNsyTmU2j.Bd.LDoPBY2DcOYFfL.XF5Yj76y8pbDGqdOYGnXZatDqvwEdJ9YT9BjAGNwWRDY50hEwwMgt.7EJI7WVmGxVmhZwvecBOwA3vY8.VTZfQqdDw5vFb8xg4vehFN6oEAA1zN5Cfvz95g4O0x0ntfTnwFNqyfxm0nAguxK9gO.Zc84pQ7Hx4U2ekkgLiBKNGRDQjJUbXLj99.UQUHaDinoNovG8b3dkTppOLVPzVo2sCFWVPBe3cMYqEEEZxQh9uaP1MTsl6Vnmd5vj5iptMJgPXrIb2gM.mLm7emUyd1KL0yQZs5.cYv5rNL3C_80.DZDj5gQfEnFvnsVNKMFRSIjyjNEN7zPQ0HuWQYv9qY_PoJvjYq9DqaUDvTFBA9PFqFE30iqZ4Dr81rPL2tcPfMP2wT3GP1org20Eysix0P2aw2ZOXijYw6CGaUU793cItHuvpjMKNBpw5.YV0J63N8tpZ3Ff8BjYcINR__4OCVe_eSyga3qmjZQ7McVu5n0Xs99qrZz8jtBf6b9hcRGK0VzzFT0VImPMUPV6WWxy0YzrOYqhCkUGEuw3n1FZgCvAeEKatZg7AfkMqkCogyUm0TuWhN4WZAuQuG2_9QB8A4YuaZiKBOBfz2_Gtkm2a9z3dNVTbazmBh7hIM.RBBk_Zvdb5FD0_Zs4jay9brCNk_VJOqPpLt4JeKk_ZbfqNLpUaWG1rbnyPF7MIPYYFHQdONT_zMGBt6TQEjqmX3C.nabrZn6Yw_W1Uxgfg6pxFGSen28VoYw6SuouNlEEy5QxMOj2KWpQevm1_qK5CIcHvECmEuDioL4Lh9DgbjKbxukhBBKthNz_GJ5hL8UGH2LXA0MbI9_L97Y8gekxU7swfIg',mdrd: 'R.0S6Qeb7Sw7q.DxFWs1H3EZoqNBp7F5LsV2g4Bhdmo-1762593149-1.2.1.1-lk7zvzl3C_gzz56sx0pMQPDHuBjJDnVVQbKj_mi5upF.Eh2Dgqo6DwP6AMgD4rlCQ46ruALveRmW44JWqYP1cC6Gu99pfem9Vktd9RusYx.q7Ifxn6nObDZwUX.cWwuX8fI.zBnB_ehA_wzulvY3BRaHVzINbnG2RwiUMrBZl4s4FLmRZIJhHVUmV0KDZ2W6BX4hsPHYbtHP0.ydHcJEyDEbZ8_aOAKeQ4mWn7ifuR9sFB62Oy4UxXyU81FrEzTTx68oZTF1km_7br8KX4DfWlVU.FXaf4naDf30HtblfOwawbbioHo4FD_lZN9IgtADT6J6snWBCw3JVhsLQmOpvDCEAUvvKDfN4SX3Og0bgFgmkp95RATID_.lGu.MAEED6FQjcXIiFmnvTVvHbZlDCl2Dm_1aPHE4QL68XdOvSeJfl4NmPidOIB7eh16orl6BeoJwLkHQuUok886F648ZlZ9jRdB1iUqBVlOcyxSdmy6RrVJlIQvM2xHfUqcdx1vF2TM1_nfz7PQLLTPzbYy8QsETE1k0xi.XsMDsB6dxXGTy2V1dwNBhrqO9_2il9mOOnaUQh5zgb9esyqTnrKgcYYexFiK8EdRWyBqAW_AU5Jg_bzzZ7JTJUzQ0HEbet1XGkZQ4ETL2tbhpQbAc1pN19qpdNtxne6tRWloCRZKRgkdrsA5X4jf_IP3hVGtBpmkKd1ev2dd_I8xSIR1iqviItMeLdzdnrLJ2dsBEszI0r0unPMFByHNlwlA71qp7LlzPzIsjDWDuubW2ubfUuiYpsfBgxKwi0yqlAw_G1mzYC5xvdvOevKsKQ6d5eD_2BaJEWWTRRzZWH9tFuppx8ztD9H71010zQJ.79_QfwFIWpOlv1ZtKNnVfSaiLPcp_IMLAyFkMw2Td1WUHtGEChRamTLUDAL54Bi_EiEqsUIdFKJOKCewwOq7ZBWw7oHWJE2i.Cw.Ks0K8uGI2S9vRfDpX03wuZSfSjrlChwAIzfNMvEcy0Ce4t2XIg671LgnetLl5l3UUL8DrwE_AGr8NARI4cVWng3kYnbYcvi4NSFWWEOTPo5mog9z5yPeDyEx8opkLz0pfPBXa4l1ZQM2BxdVwJxO1X2T2NKxr.iZaTcklemWzkwML.HZJBQQFG3bWZfSKFluyVKLpPtWjMIYo9Rjojb.MOnLwPlwTfXPtd4aPiXJNhHbCQl6G2ZtGulQ9.FIMURM.2xqjm7z6XGWqAWmcCcb0NUDbDlw_VtQ1FBvlZzvBTHqjign9DXogC66CKX40DXgDWl1MhPzkRCX5HEFFZ79QvwPVbJmJdHFnxgRf8SAhJ.7Z0lcMmxl7jFwikMQ_DRFWsvWN9lAXUzK4n3_y3mnEFIBct8nSUowjmrRi.7jbrl5jUtqjrFJioETtjhdn6BSv0EwkZb9Edo4sMwVsEhS6h_tsVIeIgvftdx2eKvSzDvv30BlN2Zj6_1IVaNfRQ_yU.l00CMW1AlKS9RO3hOeBJu42haRlGxwcDAFMAPHpAlsZb3kttRxyp4gv03ur8RvhEHP3HWKVtaxLL_VhmWFQMSz11GDRMFhCu.rj6VvknYNeKD6uoApoTbPbD3.cDyaihgBppGEqOuoOj5O7_QSbEojLvtQqmFbmtdFvjGqHzZARLwCUkqlbabh1KRsHlJ55f.9rfGvn8PMLjj.IKaPQXdADB6ltusYO9CKczSgRZAP9FMKqn7IvQy2K9YcKKZLufujfrLAY03pJQhBQy7wnZL.KZCw7beSUNO0GcE.j64jdGU.9CIfVmUFDVMYsVPcGrWqQ2dpS.qK40NAQsXp1uvmK1wm7JvpTZrXivANS3Wb48ssMPhhwS4Kne1DTzeYBvvD8CaYHJDLh0IfS5tmB9am5HqoMA2q6a5uAlMd7usiOGUmfm3S3Xkvd9dr1_2PX_AAUaV.K1pJGoKwVnizUrJzvQpRNwwXqpPhvhv1V50gRMW6FvFtPBvmQ.DCsKemXHrJGl83VdsfRF7OG71l0B8VCHmDLXepXymbSzbcViN41Yvpr7qteZoLOY6lelr8Jby__Czbt73lj6L9.eHhwHzRzkpBPl2EZGNV0_.rvQeD45kBM8OdhaO7VKk9zM5zd1VjsJQlOcEHdCIA5qpImNQ_E8Zdo2pUpgNlFfKo3HCo66sGy3ZSTK6G_HZKlzhp56s6N_e9x5hNowN14rPt4Plj4BcDzFuZOIrBBYeGqiNa6rV2i0e04CFLAp8_MQxQNizTIw2LKgacn955A3zfmNYbs6XC..fqnQgxK4LNe7VXAuHpLd0c1FKitGW7r3RcmTvJaCCnXARWDLvtl5UsA9D2mshz8jnB90WJAdqwuxarnSQCJQ4PTKyL9oJHvT2wB5mxblwVNyOm7BbE.T0rLIxHmLS67TTQKIYX5HcZTv1EMooRhXn8u5nePleaM',};var a = document.createElement('script');a.src = '.../orchestrate/chl_page/v1?ray=99b3f2f0dd1fc94a';window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, location.href.length - window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;if (window.history && window.history.replaceState) {var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;history.replaceState(null, null,"\/bins-recycling\/waste-collection-schedule\/download\/137034556?__cf_chl_rt_tk=Hj50FuHlV5.4.DGhYZYKGGD.2wOtuPl5U52sgOF1F3o-1762593149-1.0.1.1-t_hNTTko5oGkAtpBm2JtaeElpdtNesJphYc4TFU.iZ0"+ window._cf_chl_opt.cOgUHash);a.onload = function() {history.replaceState(null, null, ogU);}}document.getElementsByTagName('head')[0].appendChild(a);}());</script></body></html>': <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><style>*{box-sizing

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12........./site-packages/icalendar/parser.py:340: ValueError
View the full list of 1 ❄️ flaky test(s)
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[BostonBoroughCouncil]

Flake rate in main: 66.84% (Passed 62 times, Failed 125 times)

Stack Traces | 18.8s run time
fixturefunc = <function scrape_step at 0x7efeeb8274c0>
request = <FixtureRequest for <Function test_scenario_outline[BostonBoroughCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7efeec726420>, 'headless_mode': 'True', 'local_browser': 'False', 'selenium_url': 'http://localhost:4444'}

    def call_fixture_func(
        fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
    ) -> FixtureValue:
        if is_generator(fixturefunc):
            fixturefunc = cast(
                Callable[..., Generator[FixtureValue, None, None]], fixturefunc
            )
            generator = fixturefunc(**kwargs)
            try:
                fixture_result = next(generator)
            except StopIteration:
                raise ValueError(f"{request.fixturename} did not yield a value") from None
            finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
            request.addfinalizer(finalizer)
        else:
            fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
>           fixture_result = fixturefunc(**kwargs)

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/_pytest/fixtures.py:898: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../tests/step_defs/test_validate_council.py:101: in scrape_step
    context.parse_result = CollectData.run()
uk_bin_collection/uk_bin_collection/collect_data.py:101: in run
    return self.client_code(
uk_bin_collection/uk_bin_collection/collect_data.py:121: in client_code
    return get_bin_data_class.template_method(address_url, **kwargs)
uk_bin_collection/uk_bin_collection/get_bin_data.py:61: in template_method
    bin_data_dict = self.get_and_parse_data(this_url, **kwargs)
uk_bin_collection/uk_bin_collection/get_bin_data.py:84: in get_and_parse_data
    bin_data_dict = self.parse_data("", url=address_url, **kwargs)
.../uk_bin_collection/councils/BostonBoroughCouncil.py:95: in parse_data
    WebDriverWait(driver, 10).until(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.support.wait.WebDriverWait (session="2981e8756a9f3d24d3010a784de5c619")>
method = <function element_to_be_clickable.<locals>._predicate at 0x7efeeb6e34c0>
message = ''

    def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """Wait until the method returns a value that is not False.
    
        Calls the method provided with the driver as an argument until the
        return value does not evaluate to ``False``.
    
        Parameters:
        -----------
        method: callable(WebDriver)
            - A callable object that takes a WebDriver instance as an argument.
    
        message: str
            - Optional message for :exc:`TimeoutException`
    
        Return:
        -------
        object: T
            - The result of the last call to `method`
    
        Raises:
        -------
        TimeoutException
            - If 'method' does not return a truthy value within the WebDriverWait
            object's timeout
    
        Example:
        --------
        >>> from selenium.webdriver.common.by import By
        >>> from selenium.webdriver.support.ui import WebDriverWait
        >>> from selenium.webdriver.support import expected_conditions as EC
    
        # Wait until an element is visible on the page
        >>> wait = WebDriverWait(driver, 10)
        >>> element = wait.until(EC.visibility_of_element_located((By.ID, "exampleId")))
        >>> print(element.text)
        """
        screen = None
        stacktrace = None
    
        end_time = time.monotonic() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, "screen", None)
                stacktrace = getattr(exc, "stacktrace", None)
            if time.monotonic() > end_time:
                break
            time.sleep(self._poll)
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message: 
E       Stacktrace:
E       #0 0x5651a4a3faea <unknown>
E       #1 0x5651a448bcdb <unknown>
E       #2 0x5651a44de6c4 <unknown>
E       #3 0x5651a44de901 <unknown>
E       #4 0x5651a452d8b4 <unknown>
E       #5 0x5651a452ac87 <unknown>
E       #6 0x5651a44d0aca <unknown>
E       #7 0x5651a44d17d1 <unknown>
E       #8 0x5651a4a06ab9 <unknown>
E       #9 0x5651a4a09a8c <unknown>
E       #10 0x5651a49efd49 <unknown>
E       #11 0x5651a4a0a685 <unknown>
E       #12 0x5651a49d76c3 <unknown>
E       #13 0x5651a4a2c7d8 <unknown>
E       #14 0x5651a4a2c9b3 <unknown>
E       #15 0x5651a4a3ea83 <unknown>
E       #16 0x7f8de36bcaa4 <unknown>
E       #17 0x7f8de3749a64 __clone

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/support/wait.py:146: TimeoutException

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

📝 Add docstrings to `November_release`
@robbrad robbrad merged commit 078f1de into master Nov 8, 2025
13 of 15 checks passed
@robbrad robbrad deleted the November_release branch November 8, 2025 09:13
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (4)
uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py (1)

25-40: Consider making browser version dynamic or adding a comment.

The headers include hardcoded Chrome version "141" in both user-agent and sec-ch-ua. While this works currently, these values will become outdated over time. Consider either making them dynamic or adding a comment explaining they're static for compatibility reasons.

uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (2)

85-94: Add response structure validation.

The code assumes decoded_bins has the expected structure without validation. If the API response format changes or is malformed, this will raise KeyError.

Add validation:

             decoded_bins = self.decode_response(output)
+            
+            if not isinstance(decoded_bins, dict) or "collectionDay" not in decoded_bins:
+                raise ValueError("Invalid API response structure: missing 'collectionDay'")
+            
+            if not isinstance(decoded_bins["collectionDay"], list):
+                raise ValueError("Invalid API response: 'collectionDay' must be a list")
+            
             data: dict[str, list[dict[str, str]]] = {}
             data["bins"] = list(
                 map(
                     lambda a: {
                         "type": a["binType"],
                         "collectionDate": a["collectionDay"].replace("-", "/"),
                     },
                     decoded_bins["collectionDay"],
                 )
             )

96-100: Consider using proper logging instead of print statements.

The exception handler uses print() for error output. The codebase appears to use a logger (as seen in get_bin_data.py).

+import logging
+
+_LOGGER = logging.getLogger(__name__)
+
         except Exception as e:
-            # Here you can log the exception if needed
-            print(f"An error occurred: {e}")
-            # Optionally, re-raise the exception if you want it to propagate
+            _LOGGER.error(f"Failed to fetch Newport bin collection data: {e}")
             raise
uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py (1)

32-40: Keep kwargs["url"] as an override
Hard-coding the enviroservices endpoint means anyone relying on a config-driven url (tests, alternate environments, or future domain shifts) can no longer point the scraper elsewhere without editing code. Please keep reading the URL from kwargs and only fall back to this constant if it is missing so we retain that flexibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 475e160 and c5015cf.

📒 Files selected for processing (19)
  • .github/workflows/ha_compatibility_test.yml (1 hunks)
  • uk_bin_collection/tests/input.json (7 hunks)
  • uk_bin_collection/uk_bin_collection/councils/BostonBoroughCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/DerbyCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/HartDistrictCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/HerefordshireCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/TendringDistrictCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/WokinghamBoroughCouncil.py (2 hunks)
  • wiki/Councils.md (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py (1)
uk_bin_collection/uk_bin_collection/common.py (1)
  • create_webdriver (321-360)
uk_bin_collection/uk_bin_collection/councils/BostonBoroughCouncil.py (1)
uk_bin_collection/uk_bin_collection/common.py (1)
  • create_webdriver (321-360)
uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py (1)
uk_bin_collection/uk_bin_collection/common.py (1)
  • check_paon (52-64)
uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (2)
uk_bin_collection/uk_bin_collection/common.py (1)
  • check_uprn (67-78)
uk_bin_collection/uk_bin_collection/get_bin_data.py (1)
  • AbstractGetBinDataClass (43-146)
uk_bin_collection/uk_bin_collection/councils/TendringDistrictCouncil.py (2)
uk_bin_collection/uk_bin_collection/common.py (3)
  • check_postcode (36-49)
  • check_uprn (67-78)
  • create_webdriver (321-360)
uk_bin_collection/uk_bin_collection/get_bin_data.py (1)
  • AbstractGetBinDataClass (43-146)
uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py (3)
uk_bin_collection/uk_bin_collection/get_bin_data.py (1)
  • AbstractGetBinDataClass (43-146)
uk_bin_collection/uk_bin_collection/common.py (1)
  • check_uprn (67-78)
custom_components/uk_bin_collection/calendar.py (1)
  • event (54-63)
uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py (1)
uk_bin_collection/uk_bin_collection/common.py (1)
  • create_webdriver (321-360)
🪛 Gitleaks (8.28.0)
uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py

[high] 13-13: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 Ruff (0.14.3)
uk_bin_collection/uk_bin_collection/councils/BrightonandHoveCityCouncil.py

33-33: Local variable uprn is assigned to but never used

Remove assignment to unused variable uprn

(F841)


38-38: create_webdriver may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py

46-46: Probable use of requests call without timeout

(S113)

uk_bin_collection/uk_bin_collection/councils/BostonBoroughCouncil.py

39-39: create_webdriver may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/RochdaleCouncil.py

58-58: Prefer TypeError exception for invalid type

(TRY004)


58-58: Avoid specifying long messages outside the exception class

(TRY003)


74-74: datetime may be undefined, or defined from star imports

(F405)


77-77: datetime may be undefined, or defined from star imports

(F405)


77-77: timedelta may be undefined, or defined from star imports

(F405)


103-103: Prefer TypeError exception for invalid type

(TRY004)


103-103: Avoid specifying long messages outside the exception class

(TRY003)


105-105: Loop control variable key not used within loop body

Rename unused key to _key

(B007)


108-108: datetime may be undefined, or defined from star imports

(F405)


110-110: date_format may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/MiddlesbroughCouncil.py

7-7: from uk_bin_collection.uk_bin_collection.common import * used; unable to detect undefined names

(F403)


12-12: Unused method argument: page

(ARG002)


18-18: check_paon may be undefined, or defined from star imports

(F405)


30-30: Probable use of requests call without timeout

(S113)


39-39: f-string without any placeholders

Remove extraneous f prefix

(F541)


52-52: Probable use of requests call without timeout

(S113)


104-104: date_format may be undefined, or defined from star imports

(F405)


116-116: Consider moving this statement to an else block

(TRY300)

uk_bin_collection/uk_bin_collection/councils/HerefordshireCouncil.py

35-35: Avoid specifying long messages outside the exception class

(TRY003)


62-62: datetime may be undefined, or defined from star imports

(F405)


62-62: date_format may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/LondonBoroughSutton.py

54-54: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)


62-64: Avoid specifying long messages outside the exception class

(TRY003)

uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py

56-56: Prefer TypeError exception for invalid type

(TRY004)


56-56: Avoid specifying long messages outside the exception class

(TRY003)


69-69: datetime may be undefined, or defined from star imports

(F405)


72-72: datetime may be undefined, or defined from star imports

(F405)


72-72: timedelta may be undefined, or defined from star imports

(F405)


98-98: Prefer TypeError exception for invalid type

(TRY004)


98-98: Avoid specifying long messages outside the exception class

(TRY003)


100-100: json may be undefined, or defined from star imports

(F405)


105-105: datetime may be undefined, or defined from star imports

(F405)


107-107: date_format may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/DumfriesandGallowayCouncil.py

12-12: from uk_bin_collection.uk_bin_collection.common import * used; unable to detect undefined names

(F403)


17-17: Unused method argument: page

(ARG002)


23-23: check_uprn may be undefined, or defined from star imports

(F405)


42-42: date_format may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/ChelmsfordCityCouncil.py

12-12: from uk_bin_collection.uk_bin_collection.common import * used; unable to detect undefined names

(F403)


17-17: Unused method argument: page

(ARG002)


28-28: create_webdriver may be undefined, or defined from star imports

(F405)


45-47: try-except-pass detected, consider logging the exception

(S110)


45-45: Do not catch blind exception: Exception

(BLE001)


45-45: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


84-86: Abstract raise to an inner function

(TRY301)


84-86: Avoid specifying long messages outside the exception class

(TRY003)


103-103: date_format may be undefined, or defined from star imports

(F405)

uk_bin_collection/uk_bin_collection/councils/WokinghamBoroughCouncil.py

97-97: datetime may be undefined, or defined from star imports

(F405)


102-102: date_format may be undefined, or defined from star imports

(F405)

🔇 Additional comments (6)
.github/workflows/ha_compatibility_test.yml (1)

184-184: ✅ Safe action version upgrade.

The v5 update supports Node v24.x and is not a breaking change per se. The overwrite parameter (default 'false') is supported and will delete a matching artifact before uploading a new one if true. Since each artifact gets a unique name via the matrix variable (ha-log-${{ matrix.ha_version }}), the overwrite: true setting is harmless here, though technically unnecessary. The upgrade is fully compatible with the existing workflow.

uk_bin_collection/uk_bin_collection/councils/HartDistrictCouncil.py (1)

46-50: LGTM!

The logic correctly handles multiple bin types per row by splitting on "&" and creating separate dictionary entries with a shared collection date. The strip() call ensures clean bin type values.

uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (4)

32-47: LGTM: Encryption implementation is correct.

The AES-CBC encryption with PKCS7 padding is properly implemented using the cryptography library.


49-64: LGTM: Decryption implementation is correct.

The AES-CBC decryption with PKCS7 unpadding correctly reverses the encryption process.


68-72: LGTM: UPRN validation and input construction.

The UPRN validation and NewportInput construction with fixed literal values is correct. The check_uprn function will raise an exception for invalid UPRNs, preventing further processing.


76-84: Add timeout, response validation, and consider Content-Type header.

The HTTP request lacks resilience features used throughout the codebase:

  1. Missing timeout — all other council implementations specify timeout (10–120s); this request can hang indefinitely
  2. No response validation — response.text is accessed without checking status_code; error responses may parse incorrectly
  3. No Content-Type header — encoded body data should specify content type; verify with API documentation whether this is required

Suggested fix:

-            session = requests.Session()
-            response = session.post(
+            response = requests.post(
                 "https://iweb.itouchvision.com/portal/itouchvision/kmbd/collectionDay",
                 data=encoded_input,
+                timeout=30,
             )
+            response.raise_for_status()
 
             output = response.text

Note: Determine the correct Content-Type from API documentation (e.g., application/octet-stream if encoded_input is binary). Removing Session is also recommended since it's only used for one request.

Comment thread uk_bin_collection/uk_bin_collection/councils/HartDistrictCouncil.py
Comment on lines +43 to +44
if "bin" not in bin_type.lower():
continue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t drop sack/caddy collections

For sack-served properties this guard drops legitimate entries, so parse_data returns no general‑rubbish schedule. Example: UPRN 10024616751 renders “General rubbish - black sacks”; because it lacks the substring “bin”, this branch skips it and the bins array is incomplete. That’s a functional regression for any address served by sacks (or future caddy/bag variants). Please broaden the filter rather than hard-coding “bin”. One way:

-            if "bin" not in bin_type.lower():
+            lowered_bin_type = bin_type.lower()
+            if not any(keyword in lowered_bin_type for keyword in ("bin", "sack", "caddy")):
                 continue

Comment on lines +27 to +28
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)"}
response = requests.get(URI, headers=headers, timeout=30)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Good improvement, but verify error handling for request exceptions.

Adding the User-Agent header and timeout parameter improves robustness and follows best practices. However, the timeout parameter introduces requests.exceptions.Timeout as a potential exception. Ensure proper error handling exists for timeout and other request exceptions (e.g., ConnectionError, HTTPError).

Run the following script to check if error handling exists elsewhere in the codebase for similar request patterns:


🏁 Script executed:

#!/bin/bash
# Description: Check for error handling patterns around requests.get calls in council parsers

# Search for requests.get with try-except blocks in council files
rg -n -B2 -A5 'requests\.get' uk_bin_collection/uk_bin_collection/councils/ | rg -A5 'try:'

Length of output: 6424


I need to inspect the LondonBoroughHarrow.py file directly to verify whether error handling exists around the requests.get call.


---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Examine the context around requests.get in LondonBoroughHarrow.py

rg -n -B5 -A10 'requests\.get' uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py

Length of output: 107



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Search for LondonBoroughHarrow file in the repository

fd -i 'londonborough.*harrow' . --type f

Length of output: 69



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Examine the context around requests.get in LondonBoroughHarrow.py

rg -n -B8 -A8 'requests\.get' ./uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py

Length of output: 689


Add error handling for request timeout and exceptions.

The timeout parameter introduces requests.exceptions.Timeout that will not be caught. Additionally, response.json() (line 31) and nested dict access (line 34) lack error handling and will crash on network failures, JSON parsing errors, or missing keys. Wrap the request and JSON parsing in a try-except block, following patterns used in other council parsers (e.g., RotherhamCouncil, MidlothianCouncil).

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/LondonBoroughHarrow.py around
lines 27-28, wrap the requests.get call, response.json() and subsequent dict
accesses in a try-except block similar to RotherhamCouncil/MidlothianCouncil:
catch requests.exceptions.RequestException (including Timeout),
json.JSONDecodeError and KeyError (or TypeError) and handle them by logging the
error and returning an empty list or appropriate default so the parser doesn't
crash; ensure you still set headers and timeout, and return early on exception.

if not isinstance(rows_data, dict):
raise ValueError("Invalid data returned from API")

collections = json.loads(rows_data["jobsJSON"])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Import json before calling json.loads.

json is never imported in this module, so the first call to json.loads(...) will raise a NameError, halting the collection flow. Add the missing import near the top of the file.

 import time

 import requests
+import json

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.3)

100-100: json may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/LondonBoroughHounslow.py around
line 100, the code calls json.loads(rows_data["jobsJSON"]) but the module never
imports the json module; add "import json" near the top of the file (with other
imports) so json.loads is defined and avoid a NameError at runtime.

Comment on lines +53 to +58
if r.status_code == 429:
# manual respect if upstream Retry didn’t catch (e.g., no header)
retry_after = int(r.headers.get("Retry-After", "10"))
time.sleep(min(retry_after, 60))
r.raise_for_status()
poll += 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow the poll loop to survive 429 responses

Right after you detect a 429 you sleep, but then r.raise_for_status() runs anyway and throws HTTPError, so the loop never retries and Sutton stays broken under rate limiting. The raw int() on Retry-After will also crash if the header is a HTTP-date or decimal value. Please short-circuit the loop on 429 and make the header parsing defensive.

-            if r.status_code == 429:
-                # manual respect if upstream Retry didn’t catch (e.g., no header)
-                retry_after = int(r.headers.get("Retry-After", "10"))
-                time.sleep(min(retry_after, 60))
-            r.raise_for_status()
-            poll += 1
+            if r.status_code == 429:
+                # manual respect if upstream Retry didn't catch (e.g., no header)
+                try:
+                    retry_after = int(r.headers.get("Retry-After", "10"))
+                except (TypeError, ValueError):
+                    retry_after = 10
+                time.sleep(min(retry_after, 60))
+                poll += 1
+                continue
+            r.raise_for_status()
+            poll += 1
🧰 Tools
🪛 Ruff (0.14.3)

54-54: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)

Comment on lines +13 to +14
key_hex = "F57E76482EE3DC3336495DEDEEF3962671B054FE353E815145E29C5689F72FEC"
iv_hex = "2CBF4FC35C69B82362D393A4F0B9971A"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Remove hardcoded cryptographic keys.

Hardcoding AES encryption keys and IVs in source code is a severe security vulnerability. These credentials are now permanently exposed in version control history and accessible to anyone with repository access.

Move these to secure configuration:

-key_hex = "F57E76482EE3DC3336495DEDEEF3962671B054FE353E815145E29C5689F72FEC"
-iv_hex = "2CBF4FC35C69B82362D393A4F0B9971A"
+import os
+
+key_hex = os.environ.get("NEWPORT_API_KEY")
+iv_hex = os.environ.get("NEWPORT_API_IV")
+
+if not key_hex or not iv_hex:
+    raise ValueError("NEWPORT_API_KEY and NEWPORT_API_IV environment variables must be set")

Alternatively, use a secure secrets management solution. After moving the keys, rotate them with the API provider if possible.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key_hex = "F57E76482EE3DC3336495DEDEEF3962671B054FE353E815145E29C5689F72FEC"
iv_hex = "2CBF4FC35C69B82362D393A4F0B9971A"
import os
key_hex = os.environ.get("NEWPORT_API_KEY")
iv_hex = os.environ.get("NEWPORT_API_IV")
if not key_hex or not iv_hex:
raise ValueError("NEWPORT_API_KEY and NEWPORT_API_IV environment variables must be set")
🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 13-13: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py around
lines 13-14 the AES key and IV are hardcoded which is a critical security risk;
remove these literals and instead load the key and IV from secure configuration
(e.g., environment variables or a secrets manager), fail fast with a clear error
if they are missing, and ensure the application never logs or prints the secret
values; update any deployment/config docs to provide the secrets securely and
rotate the exposed keys with the API provider after deployment.

Comment on lines +42 to +44
params = {
"UPRN": {user_uprn},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Fix the params dictionary syntax.

Line 43 uses set literal syntax {user_uprn}, which creates a set containing the UPRN value instead of passing the value directly. This will cause the API request to fail or produce incorrect query parameters.

Apply this diff to fix the syntax:

 params = {
-    "UPRN": {user_uprn},
+    "UPRN": user_uprn,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
params = {
"UPRN": {user_uprn},
}
params = {
"UPRN": user_uprn,
}
🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py around
lines 42 to 44, the params dictionary currently uses set literal syntax ("UPRN":
{user_uprn}) which creates a set instead of passing the UPRN value; replace the
set literal with the direct value so the params dict is constructed as "UPRN":
user_uprn (ensuring user_uprn is the expected string/int type for the API).

Comment on lines +46 to 50
r = requests.get(
"https://www.southampton.gov.uk/whereilive/waste-calendar",
params=params,
headers=headers,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a timeout to prevent indefinite hangs.

The request lacks a timeout parameter, which could cause the application to hang indefinitely if the server doesn't respond. This is a reliability concern.

Apply this diff to add a timeout:

 r = requests.get(
     "https://www.southampton.gov.uk/whereilive/waste-calendar",
     params=params,
     headers=headers,
+    timeout=30,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
r = requests.get(
"https://www.southampton.gov.uk/whereilive/waste-calendar",
params=params,
headers=headers,
)
r = requests.get(
"https://www.southampton.gov.uk/whereilive/waste-calendar",
params=params,
headers=headers,
timeout=30,
)
🧰 Tools
🪛 Ruff (0.14.3)

46-46: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/SouthamptonCityCouncil.py around
lines 46 to 50, the requests.get call lacks a timeout which can cause indefinite
hangs; update the call to include a timeout parameter (e.g. timeout=10 or a
constant defined near the top) so the request fails fast on no response, and
ensure any surrounding exception handling (requests.exceptions.Timeout) is left
or added to handle timeout errors appropriately.

Comment on lines 171 to 173
bin_data["bins"].sort(
key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y")
key=lambda x: datetime.strptime(x["collectionDate"], "%d/%m/%Y")
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sort with the configured date format

You serialise dates with parsed.strftime(date_format) but re-parse them with a hard-coded "%d/%m/%Y" when sorting. If date_format ever changes (user override or future library update) this will crash. Reuse date_format in the key instead of the literal.

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/TendringDistrictCouncil.py
around lines 171 to 173, the sort key reparses collectionDate using the
hard-coded "%d/%m/%Y" while dates are serialized with
parsed.strftime(date_format); update the sort to reuse the same date_format
variable (e.g. datetime.strptime(x["collectionDate"], date_format) or
self.date_format if that's the instance attribute) so parsing matches formatting
and will not break if date_format is changed or overridden.

Comment on lines +97 to +103
dt_collection_date = datetime.strptime(
collection_date.text.strip().split(" ")[1], source_date_format
)
dict_data = {
"type": waste_type.text.strip().split("(")[0].strip(),
"collectionDate": dt_collection_date.strftime(date_format),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against single-token collection dates

collection_date.text.strip().split(" ")[1] will throw when the span renders only the bare date string (e.g. "14/11/2025") or contains double spaces, which happens for some Wokingham cards. That crashes the whole scrape. Please parse defensively by taking the last non-empty token (and validating it) before feeding it to datetime.strptime.

-                    dt_collection_date = datetime.strptime(
-                        collection_date.text.strip().split(" ")[1], source_date_format
-                    )
+                    date_text = collection_date.text.strip()
+                    parts = date_text.split()
+                    if not parts:
+                        raise ValueError(f"Unexpected date format: {date_text!r}")
+                    date_token = parts[-1].rstrip(",")
+                    dt_collection_date = datetime.strptime(
+                        date_token, source_date_format
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dt_collection_date = datetime.strptime(
collection_date.text.strip().split(" ")[1], source_date_format
)
dict_data = {
"type": waste_type.text.strip().split("(")[0].strip(),
"collectionDate": dt_collection_date.strftime(date_format),
}
date_text = collection_date.text.strip()
parts = date_text.split()
if not parts:
raise ValueError(f"Unexpected date format: {date_text!r}")
date_token = parts[-1].rstrip(",")
dt_collection_date = datetime.strptime(
date_token, source_date_format
)
dict_data = {
"type": waste_type.text.strip().split("(")[0].strip(),
"collectionDate": dt_collection_date.strftime(date_format),
}
🧰 Tools
🪛 Ruff (0.14.3)

97-97: datetime may be undefined, or defined from star imports

(F405)


102-102: date_format may be undefined, or defined from star imports

(F405)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants