record_failure() and mark_success() in security/brute_force.py call self._backend.incr_sync(...) / .reset_sync(...), but those aren't part of the async StorageBackend Protocol (incr/get/reset) — _patch_backend() monkey-patches sync shims onto MemoryBackend only, because its ops are plain dict access under the hood. RedisBackend (or any other custom StorageBackend) never gets patched, so passing backend=RedisBackend(...) to BruteForceProtection will AttributeError the first time someone fails a login.
Found while clearing mypy errors in #11 (the missing attrs were part of the tracked debt) — documented inline there but not fixed, since the real fix is making record_failure/mark_success async and awaiting the Protocol's actual incr/reset, which is a behavior change touching the middleware's call and the existing brute-force test suite, not a type-only fix.
record_failure() and mark_success() in security/brute_force.py call self._backend.incr_sync(...) / .reset_sync(...), but those aren't part of the async StorageBackend Protocol (incr/get/reset) — _patch_backend() monkey-patches sync shims onto MemoryBackend only, because its ops are plain dict access under the hood. RedisBackend (or any other custom StorageBackend) never gets patched, so passing backend=RedisBackend(...) to BruteForceProtection will AttributeError the first time someone fails a login.
Found while clearing mypy errors in #11 (the missing attrs were part of the tracked debt) — documented inline there but not fixed, since the real fix is making record_failure/mark_success async and awaiting the Protocol's actual incr/reset, which is a behavior change touching the middleware's call and the existing brute-force test suite, not a type-only fix.