Skip to content

fix: Broken councils#1762

Merged
robbrad merged 1 commit into
masterfrom
feat_dec_fixes
Dec 7, 2025
Merged

fix: Broken councils#1762
robbrad merged 1 commit into
masterfrom
feat_dec_fixes

Conversation

@robbrad
Copy link
Copy Markdown
Owner

@robbrad robbrad commented Dec 7, 2025

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling and validation across multiple council bin lookup systems.
    • Enhanced robustness for missing HTML elements to prevent lookup failures.
    • Added handling for service suspension notifications.
    • Improved date parsing with fallback strategies for postcode input location.
    • Fixed API response validation and error detection.
    • Corrected date formatting consistency in collection data.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 7, 2025

Walkthrough

This PR modifies 12 council parser files and test input data to improve robustness across bin collection workflows. Changes include error handling guards for missing page elements, response validation checks, date formatting updates, HTTP header and SSL handling, and one major migration to Selenium-based automation for EpsomandEwellBoroughCouncil.

Changes

Cohort / File(s) Summary
Selenium Integration
uk_bin_collection/tests/input.json, uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py, uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py
Added Selenium WebDriver support to input.json for EpsomandEwellBoroughCouncil; replaced direct navigation with form URL in AngusCouncil and implemented fallback strategies for postcode input location; replaced HTTP-based flow with WebDriver automation, JavaScript form injection, and dynamic scraping in EpsomandEwellBoroughCouncil.
Response & Request Validation
uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py, uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py, uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py
Added HTTP headers and status code verification in BuckinghamshireCouncil; disabled SSL verification in MedwayCouncil; added HTML error page detection in NewportCityCouncil to prevent decoding non-encrypted responses.
Error Handling Guards
uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py, uk_bin_collection/uk_bin_collection/councils/SevenoaksDistrictCouncil.py, uk_bin_collection/uk_bin_collection/councils/SouthStaffordshireDistrictCouncil.py, uk_bin_collection/uk_bin_collection/councils/WindsorAndMaidenheadCouncil.py
Added try/except guards for date parsing in NeathPortTalbotCouncil; added suspension/restart detection in SevenoaksDistrictCouncil; added guards for missing collection sections and van-collection scenarios in SouthStaffordshireDistrictCouncil; added guard for missing widget div in WindsorAndMaidenheadCouncil.
Date & Data Parsing Updates
uk_bin_collection/uk_bin_collection/councils/NewarkAndSherwoodDC.py, uk_bin_collection/uk_bin_collection/councils/SouthHollandDistrictCouncil.py
Changed BeautifulSoup initialization from page.text to raw page object in NewarkAndSherwoodDC; updated date formatting from YYYY-MM-DD to configurable target format with corresponding sort-key adjustment in SouthHollandDistrictCouncil.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebDriver
    participant Browser
    participant Portal as iTouchVision Portal
    participant BeautifulSoup

    Client->>WebDriver: create_webdriver()
    WebDriver->>Browser: initialize Selenium
    Browser->>Portal: load portal URL
    Portal->>Browser: return page HTML

    Client->>Browser: inject JavaScript to set postcode
    Browser->>Portal: trigger React form events
    Portal->>Browser: render address dropdown
    
    Client->>Browser: select address via script (UPRN)
    Browser->>Portal: update selection
    Portal->>Browser: dynamically render bin data
    
    Client->>Browser: wait for data to populate (WebDriverWait)
    Browser->>Portal: wait complete
    
    Client->>BeautifulSoup: parse page source
    BeautifulSoup->>Client: extract bin types & dates
    
    Client->>Client: parse dates & adjust year
    Client->>Client: sort results by collectionDate
    Client->>WebDriver: quit driver (cleanup)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • EpsomandEwellBoroughCouncil.py — Complex Selenium/JavaScript workflow with dynamic scraping, date parsing with year adjustment logic, and proper cleanup handling; requires verification of WebDriver instantiation and selector robustness.
  • AngusCouncil.py — Multiple fallback strategies for element location (By ID, NAME, CSS selector) with error diagnostics; verify fallback logic order and error messages.
  • BuckinghamshireCouncil.py — Hex-blob validation logic on response text; confirm the regex/validation approach is correct and handles edge cases.
  • SouthStaffordshireDistrictCouncil.py — Multiple guards with conditional element handling; verify all missing-element scenarios are covered and don't mask real errors.

Possibly related PRs

Suggested reviewers

  • dp247

Poem

🐰 A dozen councils, now refined with care,
Guards and validations everywhere!
EpsomandEwell found its Selenium way,
While others caught errors—hip-hip-hooray!
Dates parse true, and pages are checked,
Robust bin collections—now that's chef's kiss, correct!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'fix: Broken councils' is vague and generic, using non-descriptive terms that don't convey meaningful information about which councils are fixed or what specific issues were addressed. Revise the title to be more specific, such as 'fix: Update broken councils with improved error handling and robustness' or list the specific councils/issues being fixed for clarity.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat_dec_fixes

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 7, 2025

❌ 8 Tests Failed:

Tests completed Failed Passed Skipped
11 8 3 0
View the top 2 failed test(s) by shortest run time
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[SouthStaffordshireDistrictCouncil]
Stack Traces | 0.237s run time
fixturefunc = <function validate_output_step at 0x7f60d5c82de0>
request = <FixtureRequest for <Function test_scenario_outline[SouthStaffordshireDistrictCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7f60d6afc590>}

    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:114: in validate_output_step
    assert file_handler.validate_json_schema(
.../step_defs/step_helpers/file_handler.py:40: in validate_json_schema
    validate(instance=json_data, schema=schema)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

instance = {'bins': []}
schema = {'$ref': '#/definitions/BinData', '$schema': 'http://json-schema.org/draft-06/schema#', 'definitions': {'Bin': {'addit...ems': {'$ref': '#/definitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}
cls = <class 'jsonschema.validators.Draft6Validator'>, args = (), kwargs = {}
validator = Draft6Validator(schema={'$ref': '#/definitions/BinData', '$schema': 'http://json-...ft-06/schema#', 'definitions': {'B...nitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}, format_checker=None)
error = <ValidationError: '[] should be non-empty'>

    def validate(instance, schema, cls=None, *args, **kwargs):  # noqa: D417
        """
        Validate an instance under the given schema.
    
            >>> validate([2, 3, 4], {"maxItems": 2})
            Traceback (most recent call last):
                ...
            ValidationError: [2, 3, 4] is too long
    
        :func:`~jsonschema.validators.validate` will first verify that the
        provided schema is itself valid, since not doing so can lead to less
        obvious error messages and fail in less obvious or consistent ways.
    
        If you know you have a valid schema already, especially
        if you intend to validate multiple instances with
        the same schema, you likely would prefer using the
        `jsonschema.protocols.Validator.validate` method directly on a
        specific validator (e.g. ``Draft202012Validator.validate``).
    
    
        Arguments:
    
            instance:
    
                The instance to validate
    
            schema:
    
                The schema to validate with
    
            cls (jsonschema.protocols.Validator):
    
                The class that will be used to validate the instance.
    
        If the ``cls`` argument is not provided, two things will happen
        in accordance with the specification. First, if the schema has a
        :kw:`$schema` keyword containing a known meta-schema [#]_ then the
        proper validator will be used. The specification recommends that
        all schemas contain :kw:`$schema` properties for this reason. If no
        :kw:`$schema` property is found, the default validator class is the
        latest released draft.
    
        Any other provided positional and keyword arguments will be passed
        on when instantiating the ``cls``.
    
        Raises:
    
            `jsonschema.exceptions.ValidationError`:
    
                if the instance is invalid
    
            `jsonschema.exceptions.SchemaError`:
    
                if the schema itself is invalid
    
        .. rubric:: Footnotes
        .. [#] known by a validator registered with
            `jsonschema.validators.validates`
    
        """
        if cls is None:
            cls = validator_for(schema)
    
        cls.check_schema(schema)
        validator = cls(schema, *args, **kwargs)
        error = exceptions.best_match(validator.iter_errors(instance))
        if error is not None:
>           raise error
E           jsonschema.exceptions.ValidationError: [] should be non-empty
E           
E           Failed validating 'minItems' in schema['properties']['bins']:
E               {'type': 'array', 'items': {'$ref': '#/definitions/Bin'}, 'minItems': 1}
E           
E           On instance['bins']:
E               []

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/jsonschema/validators.py:1332: ValidationError
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[NewportCityCouncil]
Stack Traces | 1.06s run time
fixturefunc = <function scrape_step at 0x7fe04a50a2a0>
request = <FixtureRequest for <Function test_scenario_outline[NewportCityCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fe05ae079b0>, '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)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <NewportCityCouncil.CouncilClass object at 0x7fe05a546ae0>, _ = ''
kwargs = {'council_module_str': 'NewportCityCouncil', 'dev_mode': False, 'headless': True, 'local_browser': False, ...}
user_uprn = '100100688819'
newport_input = NewportInput(P_CLIENT_ID=130, P_COUNCIL_ID=260, P_LANG_CODE='EN', P_UPRN='100100688819')
encoded_input = '9d8693631cdc1d6ed05aeeb3e18727d1d32f00b42a1d2e61746c6a1288bf072c5a6c24c7007db57242361724eace5e7a12906bfc47622d9c14303b8c7cded3616561a8bb13c4bceb8764dc94d095b45e22216dff23b645a7d06f259b5036e00f'
session = <requests.sessions.Session object at 0x7fe04b19c8f0>
response = <Response [405]>
output = '<!DOCTYPE html>\n<html>\n<style type="text/css" media="screen">\nfooter,header{display:block;}\nhtml{font-family:sans...getElementById("reasons").remove();\n\t\t}\n       }\n       \n       noReason();\n    </script>\n</body>\n\n</html>\n'

    def parse_data(self, _: str, **kwargs) -> dict:
        """
        Fetch collection-day information for a given UPRN and return it as a normalized bins dictionary.
    
        Parameters:
            _: str
                Unused placeholder parameter kept for signature compatibility.
            kwargs:
                uprn (str): Unique Property Reference Number to query; this value is validated before use.
    
        Returns:
            dict: A dictionary with a "bins" key containing a list of mappings:
                - "type": the bin type string from the service response.
                - "collectionDate": the collection date formatted as MM/DD/YYYY.
        """
        try:
            user_uprn: str = kwargs.get("uprn") or ""
            check_uprn(user_uprn)
            newport_input = NewportInput(
                P_CLIENT_ID=130, P_COUNCIL_ID=260, P_LANG_CODE="EN", P_UPRN=user_uprn
            )
    
            encoded_input = self.encode_body(newport_input)
    
            session = requests.Session()
            response = session.post(
                "https://iweb.itouchvision..../itouchvision/kmbd/collectionDay",
                data=encoded_input,
            )
    
            output = response.text
    
            # Check if API returned HTML error page instead of encrypted data
            if output.strip().startswith('<'):
>               raise ValueError(f"API returned HTML error page instead of encrypted data. Status: {response.status_code}")
E               ValueError: API returned HTML error page instead of encrypted data. Status: 405

.../uk_bin_collection/councils/NewportCityCouncil.py:118: ValueError
View the full list of 6 ❄️ flaky test(s)
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[AngusCouncil]

Flake rate in main: 83.14% (Passed 29 times, Failed 143 times)

Stack Traces | 63.1s run time
fixturefunc = <function scrape_step at 0x7fa5c5d276a0>
request = <FixtureRequest for <Function test_scenario_outline[AngusCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fa5c77323f0>, '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/AngusCouncil.py:74: in parse_data
    address_dropdown = wait.until(EC.presence_of_element_located((By.ID, "customerAddress")))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.support.wait.WebDriverWait (session="0cfb6249e5ce40d5fd6f0540dfb865b0")>
method = <function presence_of_element_located.<locals>._predicate at 0x7fa5da12c9a0>
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 0x5633e8ad8aea <unknown>
E       #1 0x5633e8524cdb <unknown>
E       #2 0x5633e85776c4 <unknown>
E       #3 0x5633e8577901 <unknown>
E       #4 0x5633e85c68b4 <unknown>
E       #5 0x5633e85c3c87 <unknown>
E       #6 0x5633e8569aca <unknown>
E       #7 0x5633e856a7d1 <unknown>
E       #8 0x5633e8a9fab9 <unknown>
E       #9 0x5633e8aa2a8c <unknown>
E       #10 0x5633e8a88d49 <unknown>
E       #11 0x5633e8aa3685 <unknown>
E       #12 0x5633e8a706c3 <unknown>
E       #13 0x5633e8ac57d8 <unknown>
E       #14 0x5633e8ac59b3 <unknown>
E       #15 0x5633e8ad7a83 <unknown>
E       #16 0x7f1b4639daa4 <unknown>
E       #17 0x7f1b4642aa64 __clone

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/support/wait.py:146: TimeoutException
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[BuckinghamshireCouncil]

Flake rate in main: 55.26% (Passed 102 times, Failed 126 times)

Stack Traces | 2.1s run time
fixturefunc = <function scrape_step at 0x7fa5c5d276a0>
request = <FixtureRequest for <Function test_scenario_outline[BuckinghamshireCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fa5c77323f0>, '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:82: in get_and_parse_data
    bin_data_dict = self.parse_data(page, url=address_url, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <BuckinghamshireCouncil.CouncilClass object at 0x7fa5c5999910>
_ = <Response [200]>
kwargs = {'council_module_str': 'BuckinghamshireCouncil', 'dev_mode': False, 'headless': True, 'local_browser': False, ...}
user_uprn = '100081093078'
bucks_input = BucksInput(P_CLIENT_ID=152, P_COUNCIL_ID=34505, P_LANG_CODE='EN', P_UPRN='100081093078')
encoded_input = '9d8693631cdc1d6ed05aeeb3e18727d1f51ad8ecad1c44d21cfe1b91e2749538e25bc047c5552ed37aeb2d2828324be29edc9200fc5753c64fbe7c6c744559f961fda05fb7104a0ae0c86f053b5de4e7c1f5b946c11bc5226e14e93c32bc3c5b'
session = <requests.sessions.Session object at 0x7fa5c5144440>
headers = {'Content-Type': 'text/plain', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}
response = <Response [405]>

    def parse_data(self, _: str, **kwargs) -> dict:
        try:
            user_uprn: str = kwargs.get("uprn") or ""
            check_uprn(user_uprn)
            bucks_input = BucksInput(
                P_CLIENT_ID=152, P_COUNCIL_ID=34505, P_LANG_CODE="EN", P_UPRN=user_uprn
            )
    
            encoded_input = self.encode_body(bucks_input)
    
            session = requests.Session()
            headers = {
                "Content-Type": "text/plain",
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            }
            response = session.post(
                "https://itouchvision..../itouchvision/kmbd/collectionDay",
                data=encoded_input,
                headers=headers,
            )
    
            # Check if response is successful
            if response.status_code != 200:
>               raise ValueError(f"API returned status code {response.status_code}: {response.text[:200]}")
E               ValueError: API returned status code 405: <!DOCTYPE html>
E               <html>
E               <style type="text/css" media="screen">
E               footer,header{display:block;}
E               html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}
E               body{margin:0;}
E               h1{fon

.../uk_bin_collection/councils/BuckinghamshireCouncil.py:89: ValueError
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[NeathPortTalbotCouncil]

Flake rate in main: 78.83% (Passed 47 times, Failed 175 times)

Stack Traces | 15.1s run time
fixturefunc = <function scrape_step at 0x7f4d9b5f2660>
request = <FixtureRequest for <Function test_scenario_outline[NeathPortTalbotCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7f4d9c53dc70>, '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/NeathPortTalbotCouncil.py:40: in parse_data
    cookieAccept.click()
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/remote/webelement.py:119: in click
    self._execute(Command.CLICK_ELEMENT)
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/remote/webelement.py:572: in _execute
    return self._parent.execute(command, params)
../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/remote/webdriver.py:429: in execute
    self.error_handler.check_response(response)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f4d9b76da90>
response = {'status': 400, 'value': '{"value":{"error":"element click intercepted","message":"element click intercepted: Element ...unknown>\\n#18 0x561cd4ecea83 \\u003Cunknown>\\n#19 0x7fb9dc0d0aa4 \\u003Cunknown>\\n#20 0x7fb9dc15da64 __clone\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -> None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "<anonymous>")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "<anonymous>")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.ElementClickInterceptedException: Message: element click intercepted: Element is not clickable at point (1597, 957)
E         (Session info: chrome=142.0.7444.59)
E       Stacktrace:
E       #0 0x561cd4ecfaea <unknown>
E       #1 0x561cd491bcdb <unknown>
E       #2 0x561cd49755ac <unknown>
E       #3 0x561cd4973402 <unknown>
E       #4 0x561cd4970a92 <unknown>
E       #5 0x561cd497012f <unknown>
E       #6 0x561cd4962a0b <unknown>
E       #7 0x561cd4962387 <unknown>
E       #8 0x561cd49bac87 <unknown>
E       #9 0x561cd4960aca <unknown>
E       #10 0x561cd49617d1 <unknown>
E       #11 0x561cd4e96ab9 <unknown>
E       #12 0x561cd4e99a8c <unknown>
E       #13 0x561cd4e7fd49 <unknown>
E       #14 0x561cd4e9a685 <unknown>
E       #15 0x561cd4e676c3 <unknown>
E       #16 0x561cd4ebc7d8 <unknown>
E       #17 0x561cd4ebc9b3 <unknown>
E       #18 0x561cd4ecea83 <unknown>
E       #19 0x7fb9dc0d0aa4 <unknown>
E       #20 0x7fb9dc15da64 __clone

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../webdriver/remote/errorhandler.py:232: ElementClickInterceptedException
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[NewarkAndSherwoodDC]

Flake rate in main: 100.00% (Passed 0 times, Failed 210 times)

Stack Traces | 0.008s run time
fixturefunc = <function validate_output_step at 0x7f4d9b5f2160>
request = <FixtureRequest for <Function test_scenario_outline[NewarkAndSherwoodDC]>>
kwargs = {'context': <test_validate_council.Context object at 0x7f4d9c53dc70>}

    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:114: in validate_output_step
    assert file_handler.validate_json_schema(
.../step_defs/step_helpers/file_handler.py:40: in validate_json_schema
    validate(instance=json_data, schema=schema)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

instance = {'bins': []}
schema = {'$ref': '#/definitions/BinData', '$schema': 'http://json-schema.org/draft-06/schema#', 'definitions': {'Bin': {'addit...ems': {'$ref': '#/definitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}
cls = <class 'jsonschema.validators.Draft6Validator'>, args = (), kwargs = {}
validator = Draft6Validator(schema={'$ref': '#/definitions/BinData', '$schema': 'http://json-...ft-06/schema#', 'definitions': {'B...nitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}, format_checker=None)
error = <ValidationError: '[] should be non-empty'>

    def validate(instance, schema, cls=None, *args, **kwargs):  # noqa: D417
        """
        Validate an instance under the given schema.
    
            >>> validate([2, 3, 4], {"maxItems": 2})
            Traceback (most recent call last):
                ...
            ValidationError: [2, 3, 4] is too long
    
        :func:`~jsonschema.validators.validate` will first verify that the
        provided schema is itself valid, since not doing so can lead to less
        obvious error messages and fail in less obvious or consistent ways.
    
        If you know you have a valid schema already, especially
        if you intend to validate multiple instances with
        the same schema, you likely would prefer using the
        `jsonschema.protocols.Validator.validate` method directly on a
        specific validator (e.g. ``Draft202012Validator.validate``).
    
    
        Arguments:
    
            instance:
    
                The instance to validate
    
            schema:
    
                The schema to validate with
    
            cls (jsonschema.protocols.Validator):
    
                The class that will be used to validate the instance.
    
        If the ``cls`` argument is not provided, two things will happen
        in accordance with the specification. First, if the schema has a
        :kw:`$schema` keyword containing a known meta-schema [#]_ then the
        proper validator will be used. The specification recommends that
        all schemas contain :kw:`$schema` properties for this reason. If no
        :kw:`$schema` property is found, the default validator class is the
        latest released draft.
    
        Any other provided positional and keyword arguments will be passed
        on when instantiating the ``cls``.
    
        Raises:
    
            `jsonschema.exceptions.ValidationError`:
    
                if the instance is invalid
    
            `jsonschema.exceptions.SchemaError`:
    
                if the schema itself is invalid
    
        .. rubric:: Footnotes
        .. [#] known by a validator registered with
            `jsonschema.validators.validates`
    
        """
        if cls is None:
            cls = validator_for(schema)
    
        cls.check_schema(schema)
        validator = cls(schema, *args, **kwargs)
        error = exceptions.best_match(validator.iter_errors(instance))
        if error is not None:
>           raise error
E           jsonschema.exceptions.ValidationError: [] should be non-empty
E           
E           Failed validating 'minItems' in schema['properties']['bins']:
E               {'type': 'array', 'items': {'$ref': '#/definitions/Bin'}, 'minItems': 1}
E           
E           On instance['bins']:
E               []

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/jsonschema/validators.py:1332: ValidationError
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[SouthHollandDistrictCouncil]

Flake rate in main: 78.71% (Passed 43 times, Failed 159 times)

Stack Traces | 0.372s run time
self = <Response [403]>, kwargs = {}

    def json(self, **kwargs):
        r"""Returns the json-encoded content of a response, if any.
    
        :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
        :raises requests.exceptions.JSONDecodeError: If the response body does not
            contain valid json.
        """
    
        if not self.encoding and self.content and len(self.content) > 3:
            # No encoding set. JSON RFC 4627 section 3 states we should expect
            # UTF-8, -16 or -32. Detect which one to use; If the detection or
            # decoding fails, fall back to `self.text` (using charset_normalizer to make
            # a best guess).
            encoding = guess_json_utf(self.content)
            if encoding is not None:
                try:
                    return complexjson.loads(self.content.decode(encoding), **kwargs)
                except UnicodeDecodeError:
                    # Wrong UTF codec detected; usually because it's not UTF-8
                    # but some other 8-bit codec.  This is an RFC violation,
                    # and the server didn't bother to tell us what codec *was*
                    # used.
                    pass
                except JSONDecodeError as e:
                    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
    
        try:
>           return complexjson.loads(self.text, **kwargs)

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12....../site-packages/requests/models.py:971: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
....../usr/lib/python3.12/json/__init__.py:346: in loads
    return _default_decoder.decode(s)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.decoder.JSONDecoder object at 0x7fe06054dcd0>, s = '403 Forbidden'
_w = <built-in method match of re.Pattern object at 0x7fe060218ba0>

    def decode(self, s, _w=WHITESPACE.match):
        """Return the Python representation of ``s`` (a ``str`` instance
        containing a JSON document).
    
        """
        obj, end = self.raw_decode(s, idx=_w(s, 0).end())
        end = _w(s, end).end()
        if end != len(s):
>           raise JSONDecodeError("Extra data", s, end)
E           json.decoder.JSONDecodeError: Extra data: line 1 column 5 (char 4)

....../usr/lib/python3.12/json/decoder.py:340: JSONDecodeError

During handling of the above exception, another exception occurred:

fixturefunc = <function scrape_step at 0x7fe04a50a2a0>
request = <FixtureRequest for <Function test_scenario_outline[SouthHollandDistrictCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fe05ae079b0>, '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/SouthHollandDistrictCouncil.py:50: in parse_data
    bin_collection = response.json()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [403]>, kwargs = {}

    def json(self, **kwargs):
        r"""Returns the json-encoded content of a response, if any.
    
        :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
        :raises requests.exceptions.JSONDecodeError: If the response body does not
            contain valid json.
        """
    
        if not self.encoding and self.content and len(self.content) > 3:
            # No encoding set. JSON RFC 4627 section 3 states we should expect
            # UTF-8, -16 or -32. Detect which one to use; If the detection or
            # decoding fails, fall back to `self.text` (using charset_normalizer to make
            # a best guess).
            encoding = guess_json_utf(self.content)
            if encoding is not None:
                try:
                    return complexjson.loads(self.content.decode(encoding), **kwargs)
                except UnicodeDecodeError:
                    # Wrong UTF codec detected; usually because it's not UTF-8
                    # but some other 8-bit codec.  This is an RFC violation,
                    # and the server didn't bother to tell us what codec *was*
                    # used.
                    pass
                except JSONDecodeError as e:
                    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
    
        try:
            return complexjson.loads(self.text, **kwargs)
        except JSONDecodeError as e:
            # Catch JSON-related errors and raise as requests.JSONDecodeError
            # This aliases json.JSONDecodeError and simplejson.JSONDecodeError
>           raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
E           requests.exceptions.JSONDecodeError: Extra data: line 1 column 5 (char 4)

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12....../site-packages/requests/models.py:975: JSONDecodeError
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[WindsorAndMaidenheadCouncil]

Flake rate in main: 89.73% (Passed 23 times, Failed 201 times)

Stack Traces | 58.2s run time
fixturefunc = <function validate_output_step at 0x7f60d5c82de0>
request = <FixtureRequest for <Function test_scenario_outline[WindsorAndMaidenheadCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7f60d6afc590>}

    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:114: in validate_output_step
    assert file_handler.validate_json_schema(
.../step_defs/step_helpers/file_handler.py:40: in validate_json_schema
    validate(instance=json_data, schema=schema)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

instance = {'bins': []}
schema = {'$ref': '#/definitions/BinData', '$schema': 'http://json-schema.org/draft-06/schema#', 'definitions': {'Bin': {'addit...ems': {'$ref': '#/definitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}
cls = <class 'jsonschema.validators.Draft6Validator'>, args = (), kwargs = {}
validator = Draft6Validator(schema={'$ref': '#/definitions/BinData', '$schema': 'http://json-...ft-06/schema#', 'definitions': {'B...nitions/Bin'}, 'minItems': 1, 'type': 'array'}}, 'required': ['bins'], 'title': 'BinData', ...}}}, format_checker=None)
error = <ValidationError: '[] should be non-empty'>

    def validate(instance, schema, cls=None, *args, **kwargs):  # noqa: D417
        """
        Validate an instance under the given schema.
    
            >>> validate([2, 3, 4], {"maxItems": 2})
            Traceback (most recent call last):
                ...
            ValidationError: [2, 3, 4] is too long
    
        :func:`~jsonschema.validators.validate` will first verify that the
        provided schema is itself valid, since not doing so can lead to less
        obvious error messages and fail in less obvious or consistent ways.
    
        If you know you have a valid schema already, especially
        if you intend to validate multiple instances with
        the same schema, you likely would prefer using the
        `jsonschema.protocols.Validator.validate` method directly on a
        specific validator (e.g. ``Draft202012Validator.validate``).
    
    
        Arguments:
    
            instance:
    
                The instance to validate
    
            schema:
    
                The schema to validate with
    
            cls (jsonschema.protocols.Validator):
    
                The class that will be used to validate the instance.
    
        If the ``cls`` argument is not provided, two things will happen
        in accordance with the specification. First, if the schema has a
        :kw:`$schema` keyword containing a known meta-schema [#]_ then the
        proper validator will be used. The specification recommends that
        all schemas contain :kw:`$schema` properties for this reason. If no
        :kw:`$schema` property is found, the default validator class is the
        latest released draft.
    
        Any other provided positional and keyword arguments will be passed
        on when instantiating the ``cls``.
    
        Raises:
    
            `jsonschema.exceptions.ValidationError`:
    
                if the instance is invalid
    
            `jsonschema.exceptions.SchemaError`:
    
                if the schema itself is invalid
    
        .. rubric:: Footnotes
        .. [#] known by a validator registered with
            `jsonschema.validators.validates`
    
        """
        if cls is None:
            cls = validator_for(schema)
    
        cls.check_schema(schema)
        validator = cls(schema, *args, **kwargs)
        error = exceptions.best_match(validator.iter_errors(instance))
        if error is not None:
>           raise error
E           jsonschema.exceptions.ValidationError: [] should be non-empty
E           
E           Failed validating 'minItems' in schema['properties']['bins']:
E               {'type': 'array', 'items': {'$ref': '#/definitions/Bin'}, 'minItems': 1}
E           
E           On instance['bins']:
E               []

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.12/lib/python3.12.../site-packages/jsonschema/validators.py:1332: ValidationError

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

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: 6

🧹 Nitpick comments (3)
uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py (1)

87-95: Good validation logic with optional refactor opportunity.

The status code and hex validation checks effectively prevent downstream errors. Both validations provide helpful error messages with response previews.

The hex validation could be simplified using a try/except with the bytes.fromhex() call that happens later (line 53). This would be more efficient and avoid checking twice:

-            # Check if output looks like hex (should only contain hex characters)
-            if not all(c in '0123456789ABCDEFabcdef' for c in output.strip()):
-                raise ValueError(f"API returned non-hex response (status {response.status_code}). Response starts with: {output[:200]}")
-
-            decoded_bins = self.decode_response(output)
+            try:
+                decoded_bins = self.decode_response(output)
+            except ValueError as e:
+                raise ValueError(f"API returned non-hex response (status {response.status_code}). Response starts with: {output[:200]}") from e

However, the current approach is explicit and readable, so this refactor is entirely optional.

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

31-32: Direct navigation improves efficiency.

Navigating directly to the form URL removes unnecessary intermediate steps. The 3-second sleep allows for page load, though this is a fixed delay that might be sub-optimal for varying network conditions.

Consider replacing the fixed sleep with a more robust wait condition:

# Instead of time.sleep(3), use an explicit wait
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")

This would wait only as long as necessary for the page to fully load.

Also applies to: 40-42

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

107-140: Good guarded date parsing; consider also guarding sibling lookups and imports

The new logic to:

  • read date_text,
  • skip "Bank Holidays",
  • and wrap datetime.strptime(...) in a try/except ValueError

is a solid way to ignore non‑date <h2> elements and notice popups without breaking parsing.

Two small follow‑ups you might consider:

  1. Guard the sibling container
    date.find_next_sibling("div") can still return None if the markup changes. A quick guard would align with the rest of this defensive change:
-        bin_types_wrapper = date.find_next_sibling("div")
-        for bin_type_wrapper in bin_types_wrapper.find_all(
+        bin_types_wrapper = date.find_next_sibling("div")
+        if not bin_types_wrapper:
+            continue
+        for bin_type_wrapper in bin_types_wrapper.find_all(
             "div",
             {
                 "class": "card-body ps-5 ps-md-4 ps-lg-5 position-relative bg-white"
             },
         ):
  1. Address Ruff F405 for datetime/date_format
    To satisfy the static analysis hint and make dependencies explicit, you could replace the star import with explicit ones, e.g.:
-from uk_bin_collection.uk_bin_collection.common import *
+from datetime import datetime
+from uk_bin_collection.uk_bin_collection.common import create_webdriver, date_format, check_paon, check_postcode

(and adjust any other names used from common).

These are incremental, non‑blocking cleanups on top of a good robustness fix.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35a08d1 and 15d8390.

📒 Files selected for processing (12)
  • uk_bin_collection/tests/input.json (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py (2 hunks)
  • uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/NewarkAndSherwoodDC.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/SevenoaksDistrictCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/SouthHollandDistrictCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/SouthStaffordshireDistrictCouncil.py (1 hunks)
  • uk_bin_collection/uk_bin_collection/councils/WindsorAndMaidenheadCouncil.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py (1)
uk_bin_collection/uk_bin_collection/common.py (2)
  • check_uprn (67-78)
  • create_webdriver (321-360)
🪛 Ruff (0.14.7)
uk_bin_collection/uk_bin_collection/councils/SouthHollandDistrictCouncil.py

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

(F405)


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

(F405)


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

(F405)


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

(F405)

uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py

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

(F405)


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

(F405)


41-41: Local variable postcode_input is assigned to but never used

Remove assignment to unused variable postcode_input

(F841)


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

(TRY003)


66-66: Local variable address_select is assigned to but never used

Remove assignment to unused variable address_select

(F841)


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

(F405)


139-139: Do not catch blind exception: Exception

(BLE001)

uk_bin_collection/uk_bin_collection/councils/NeathPortTalbotCouncil.py

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

(F405)


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

(F405)


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

(F405)

uk_bin_collection/uk_bin_collection/councils/BuckinghamshireCouncil.py

89-89: Abstract raise to an inner function

(TRY301)


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

(TRY003)


95-95: Abstract raise to an inner function

(TRY301)


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

(TRY003)

uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py

23-23: Probable use of requests call without timeout

(S113)


23-23: Probable use of requests call with verify=False disabling SSL certificate checks

(S501)

uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py

57-57: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


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

(TRY003)

uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py

118-118: Abstract raise to an inner function

(TRY301)


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

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Integration Tests (3.12, 1.8.4)
  • GitHub Check: Run Unit Tests (3.12, 1.8.4)
🔇 Additional comments (9)
uk_bin_collection/uk_bin_collection/councils/NewportCityCouncil.py (1)

115-118: LGTM - Good defensive validation!

The HTML error page detection is a solid guard before attempting decryption. The check correctly identifies when the API returns HTML instead of encrypted data.

The static analysis hints about abstracting the raise and message length are stylistic suggestions that can be optionally addressed if this pattern is used frequently across the codebase.

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

77-85: LGTM - Headers enhance API compatibility.

The addition of headers, particularly the User-Agent, may help ensure successful API responses if the service validates or adjusts behavior based on client identification. The Content-Type: text/plain is appropriate for the hex-encoded payload.

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

1-11: LGTM - Imports support the Selenium migration.

The new imports correctly support the transition to a Selenium-based workflow. The star import from common is flagged by static analysis but appears to be a consistent pattern across this codebase.


143-151: LGTM - Proper cleanup and useful sorting.

Sorting the bins by collection date improves usability, and the finally block ensures the WebDriver is properly cleaned up even if errors occur.

uk_bin_collection/tests/input.json (1)

902-911: LGTM - Test data correctly reflects Selenium migration.

The updated test configuration properly supports the new Selenium-based workflow for EpsomandEwellBoroughCouncil. The addition of the postcode parameter, skip_get_url, web driver URL, and updated documentation all align with the implementation changes.

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

18-18: The change is correct and fixes broken code.

The page parameter is indeed a string (as indicated by the type hint and base class signature), not a response object. The old code calling page.text would have failed at runtime because strings don't have a .text attribute. This change correctly uses the string directly, which aligns with the declared type hint page: str, the base class contract in AbstractGetBinDataClass.parse_data(), and how all other council implementations handle this parameter. No type hint updates are needed.

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

40-42: Defensive guard correctly handles missing widget-bin-collections container

Early‑returning an empty data dict when next_collection_div is absent avoids AttributeError on the subsequent .find_all() calls, and still allows the finally block to close the driver. This is a clean, low‑risk robustness improvement.

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

84-86: Skipping suspended/restarting services avoids bogus date parsing

Checking raw_next_collection_date.lower() for "suspended"/"restarting" and continue‑ing before parse(...) is a sensible guard against status messages that don’t contain a valid date, and keeps the rest of the logic unchanged.

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

70-79: Robust guards around collection sections and table structure look good

The added checks:

  • returning early when showCollectionDates is missing,
  • short‑circuiting for the “van collection” message,
  • only reading the next date/type when their elements exist, and
  • only iterating the leisure-table when present and with exactly two <td> cells per row,

all significantly reduce the chance of AttributeError/index errors from missing or variant markup while cleanly reusing add_bin_types_to_collection. This is a solid resilience improvement for this scraper.

Also applies to: 80-89, 94-107

Comment on lines +44 to +57
# Try to find the postcode input with different selectors
try:
close_button = wait.until(EC.element_to_be_clickable((By.TAG_NAME, "button")))
if close_button.text.strip().lower() in ['close', 'dismiss', 'ok']:
close_button.click()
postcode_input = wait.until(EC.element_to_be_clickable((By.ID, "searchString")))
except TimeoutException:
pass

# Wait for postcode input to be clickable
postcode_input = wait.until(EC.element_to_be_clickable((By.ID, "searchString")))
# Try alternative selectors
try:
postcode_input = driver.find_element(By.NAME, "searchString")
except NoSuchElementException:
try:
postcode_input = driver.find_element(By.CSS_SELECTOR, "input[type='text']")
except NoSuchElementException:
# Print page source for debugging
print("Page source:", driver.page_source[:1000])
raise ValueError("Could not find postcode input field")
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.

🛠️ Refactor suggestion | 🟠 Major

Robust fallback strategy with minor improvement needed.

The multi-selector approach significantly improves reliability when the page structure varies. The page source debugging output is helpful.

Add the from err clause to preserve the exception chain as suggested by static analysis (B904):

                     try:
                         postcode_input = driver.find_element(By.CSS_SELECTOR, "input[type='text']")
                     except NoSuchElementException:
                         # Print page source for debugging
                         print("Page source:", driver.page_source[:1000])
-                        raise ValueError("Could not find postcode input field")
+                        raise ValueError("Could not find postcode input field") from None

Note: The final CSS selector input[type='text'] is very broad and might match an unintended input. Consider adding more specificity if this causes issues (e.g., form input[type='text'] or similar).

🧰 Tools
🪛 Ruff (0.14.7)

57-57: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


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

(TRY003)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/AngusCouncil.py around lines 44
to 57, the nested NoSuchElementException handler raises a new ValueError without
preserving the original exception chain; update the except clause to capture the
caught exception into a variable (e.g., except NoSuchElementException as e:) and
then re-raise the ValueError using "raise ValueError('Could not find postcode
input field') from e" so the original exception context is retained.

Comment on lines +40 to +51
# Wait for postcode input to be present
postcode_input = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "postcodeSearch"))
)

# Enter postcode using JavaScript to trigger React events
if user_postcode:
postcode = user_postcode
else:
# If no postcode provided, we need to derive it from UPRN
# For now, raise an error
raise ValueError("Postcode is required for EpsomandEwellBoroughCouncil")
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.

🛠️ Refactor suggestion | 🟠 Major

Remove unused variable and validate postcode requirement.

The postcode validation correctly enforces a required parameter for this Selenium-based flow.

Remove the unused postcode_input variable assignment (flagged by Ruff F841):

-            # Wait for postcode input to be present
-            postcode_input = WebDriverWait(driver, 10).until(
-                EC.presence_of_element_located((By.ID, "postcodeSearch"))
-            )
+            # Wait for postcode input to be present
+            WebDriverWait(driver, 10).until(
+                EC.presence_of_element_located((By.ID, "postcodeSearch"))
+            )

The wait is still performed, but without storing the unnecessary reference.

🧰 Tools
🪛 Ruff (0.14.7)

41-41: Local variable postcode_input is assigned to but never used

Remove assignment to unused variable postcode_input

(F841)


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

(TRY003)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py
around lines 40 to 51, remove the unused postcode_input variable to satisfy Ruff
F841 by performing the WebDriverWait call without assigning its result (e.g.
call WebDriverWait(...).until(...) directly), and keep the existing postcode
validation logic that raises ValueError when no user_postcode is provided.

Comment on lines +53 to +79
driver.execute_script(f"""
const input = document.getElementById('postcodeSearch');
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, '{postcode}');
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
""")

# Click the Find button
find_button = driver.find_element(By.CSS_SELECTOR, ".govuk-button")
find_button.click()

# Wait for address dropdown to appear and be populated
address_select = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "addressSelect"))
)

# Wait a bit for options to populate
import time
time.sleep(2)

# Select the address by UPRN value using JavaScript
driver.execute_script(f"""
const select = document.getElementById('addressSelect');
select.value = '{user_uprn}';
select.dispatchEvent(new Event('change', {{ bubbles: true }}));
""")
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.

🛠️ Refactor suggestion | 🟠 Major

JavaScript approach is appropriate for React, but clean up unused variable.

The JavaScript-based form manipulation correctly handles React's synthetic events. Using nativeInputValueSetter and dispatching events ensures React's state updates properly.

Remove the unused address_select variable (flagged by Ruff F841):

-            # Wait for address dropdown to appear and be populated
-            address_select = WebDriverWait(driver, 10).until(
-                EC.presence_of_element_located((By.ID, "addressSelect"))
-            )
+            # Wait for address dropdown to appear and be populated
+            WebDriverWait(driver, 10).until(
+                EC.presence_of_element_located((By.ID, "addressSelect"))
+            )

The time.sleep(2) on line 72 is a fixed delay. Consider using an explicit wait for dropdown options to be populated:

# Wait for dropdown to have options
WebDriverWait(driver, 10).until(
    lambda d: len(d.find_element(By.ID, "addressSelect").find_elements(By.TAG_NAME, "option")) > 1
)
🧰 Tools
🪛 Ruff (0.14.7)

66-66: Local variable address_select is assigned to but never used

Remove assignment to unused variable address_select

(F841)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py
around lines 53 to 79, remove the unused local variable address_select (Ruff
F841) and the time.sleep(2) fixed delay; instead, after clicking the Find button
use an explicit WebDriverWait that waits until the addressSelect element has
more than one option (or the expected number), then proceed to set select.value
and dispatch the change via execute_script; also remove the unnecessary import
time if no longer used.

Comment on lines 111 to 141
try:
collection_date = date_text.text.strip().split(":")[1]
except IndexError:
# Extract day and month from "Thursday 11 December" format
match = re.search(r'(\w+)\s+(\d{1,2})\s+(\w+)', date_text)
if match:
day = match.group(2)
month = match.group(3)

# Determine the year (if month is in the past, use next year)
current_date = datetime.now()
current_year = current_date.year

# Try parsing with current year
try:
date_obj = datetime.strptime(f"{day} {month} {current_year}", "%d %B %Y")
# If the date is more than 30 days in the past, assume it's next year
if (current_date - date_obj).days > 30:
date_obj = datetime.strptime(f"{day} {month} {current_year + 1}", "%d %B %Y")
except ValueError:
# Try with next year
date_obj = datetime.strptime(f"{day} {month} {current_year + 1}", "%d %B %Y")

collection_date = date_obj.strftime(date_format)

dict_data = {
"type": bin_type,
"collectionDate": collection_date,
}
bindata["bins"].append(dict_data)
except Exception as e:
print(f"Error parsing date '{date_text}': {e}")
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.

🛠️ Refactor suggestion | 🟠 Major

Date parsing logic is sound but exception handling is too broad.

The date parsing correctly handles year boundaries, trying the current year first and using next year if the date appears to be more than 30 days in the past.

Replace the bare except Exception with specific exception types (flagged by Ruff BLE001):

                 try:
                     # Extract day and month from "Thursday 11 December" format
                     match = re.search(r'(\w+)\s+(\d{1,2})\s+(\w+)', date_text)
                     if match:
                         # ... date parsing logic ...
                         dict_data = {
                             "type": bin_type,
                             "collectionDate": collection_date,
                         }
                         bindata["bins"].append(dict_data)
-                except Exception as e:
+                except (ValueError, AttributeError) as e:
                     print(f"Error parsing date '{date_text}': {e}")
                     continue

This catches the specific exceptions that could occur during date parsing while letting unexpected errors propagate.

🧰 Tools
🪛 Ruff (0.14.7)

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

(F405)


139-139: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/EpsomandEwellBoroughCouncil.py
around lines 111 to 141, replace the broad "except Exception" that swallows all
errors with specific exceptions that can occur during the regex/date parsing
(for example ValueError from datetime.strptime and re.error from the regex
library); catch only those exceptions, log the error with the existing print
message, and let other unexpected exceptions propagate so they are not silently
ignored.

api_url = f"https://api.medway.gov.uk/api/waste/getwasteday/{user_uprn}"

response = requests.get(api_url)
response = requests.get(api_url, verify=False)
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

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the context around line 23
cat -n uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py | sed -n '15,35p'

Repository: robbrad/UKBinCollectionData

Length of output: 934


🏁 Script executed:

# Get the full context of the function containing line 23
cat -n uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py | head -50

Repository: robbrad/UKBinCollectionData

Length of output: 1642


🏁 Script executed:

# Search for all requests.get calls with verify=False in the codebase
rg "requests\.get.*verify\s*=\s*False" -A 2 -B 2

Repository: robbrad/UKBinCollectionData

Length of output: 3565


🏁 Script executed:

# Check for requests.get calls without timeout parameter
rg "requests\.(get|post|request)" --no-filename | head -20

Repository: robbrad/UKBinCollectionData

Length of output: 1086


Add timeout and make TLS verification configurable in Medway API call

The hardcoded verify=False disables TLS verification, creating a security risk. Additionally, the missing timeout can cause hangs if the Medway API stalls. Since the function already uses **kwargs, both should be made configurable with secure defaults:

-        response = requests.get(api_url, verify=False)
+        timeout = kwargs.get("timeout", 10)
+        verify_ssl = kwargs.get("verify_ssl", True)
+        response = requests.get(api_url, timeout=timeout, verify=verify_ssl)

This addresses the Ruff S113 (missing timeout) and S501 (verify=False) warnings. If verify=False was added to work around a certificate issue on Medway's side, consider gating it behind config (environment variable or HA option) and adding a TODO comment for future removal.

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

🧰 Tools
🪛 Ruff (0.14.7)

23-23: Probable use of requests call without timeout

(S113)


23-23: Probable use of requests call with verify=False disabling SSL certificate checks

(S501)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/MedwayCouncil.py around line 23,
the requests.get call uses a hardcoded verify=False and no timeout (triggering
Ruff S501 and S113); change it to accept TLS verification and timeout via kwargs
with secure defaults (verify defaulting to True and timeout defaulting to a sane
value like 10s) so callers can override them, or read a config/env var to allow
verify=False only when explicitly set; update the requests.get call to pass
verify=kwargs.pop('verify', config_or_env_default) and
timeout=kwargs.pop('timeout', 10) (or similar), and add a short TODO noting that
verify=False is a temporary workaround if used.

Comment on lines +57 to +59
# Convert date from YYYY-MM-DD to DD/MM/YYYY format
parsed_date = datetime.strptime(collection_date, "%Y-%m-%d")

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 | 🟡 Minor

Runtime date handling is consistent, but docstring now lies about format

The new logic to:

  • parse collection_date with datetime.strptime(..., "%Y-%m-%d"),
  • store collectionDate using parsed_date.strftime(date_format), and
  • sort using datetime.strptime(..., date_format)

is internally consistent and matches the shared date_format.

However, the docstring above still says "collectionDate" ... in "YYYY-MM-DD" format and that sorting is based on that format. That’s now incorrect and can confuse callers.

You could update it along these lines:

-                - "collectionDate" (str | None): Collection date in "YYYY-MM-DD" format.
-                The list is sorted in ascending order by "collectionDate".
+                - "collectionDate" (str | None): Collection date formatted using `date_format` (e.g. "DD/MM/YYYY").
+                The list is sorted in ascending order by "collectionDate".

Optionally, to satisfy Ruff’s F405 hints, you can also import datetime and date_format explicitly instead of relying on the star import from common.

Also applies to: 62-62, 67-67

🧰 Tools
🪛 Ruff (0.14.7)

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

(F405)

🤖 Prompt for AI Agents
In uk_bin_collection/uk_bin_collection/councils/SouthHollandDistrictCouncil.py
around lines 57-59 (and also apply same change at lines 62 and 67), update the
docstring to reflect that collectionDate is returned in the shared date_format
(DD/MM/YYYY) rather than "YYYY-MM-DD" and that sorting/parsing uses that
date_format; also replace the star import from common by explicitly importing
datetime and date_format (e.g., from datetime import datetime and from
uk_bin_collection.common import date_format) to satisfy lint F405 and avoid
relying on wildcard imports.

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.

1 participant