Skip to content

Handle dbi item groups that EduPage serialises as an empty array - #126

Merged
ivanhrabcak merged 3 commits into
EdupageAPI:masterfrom
Koky05:fix-dbi-array-item-groups
Oct 11, 2026
Merged

ivanhrabcak merged 3 commits into
EdupageAPI:masterfrom
Koky05:fix-dbi-array-item-groups

Conversation

@Koky05

@Koky05 Koky05 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Handle dbi item groups that EduPage serialises as an empty array

Problem

Since 2026-10-08, get_classes() and get_timetable() fail for some accounts:

get_classes failed: 'list' object has no attribute 'get'
EDUPAGE error updating get_timetable() data for 2026-10-08: 'list' object has no attribute 'get'
Failed to fetch timetable data for 2026-10-08: ...

dbi itself is still an object. The change is one level deeper: EduPage now
serialises a dbi item group that has no entries as an empty JSON array
([]) instead of an empty object ({}).

DbiHelper.__get_item_with_id() does items_in_group.get(str(item_id)), so
when the group arrives as an array that call raises AttributeError. The
groups reached through .get() are teachers, students, subjects,
classrooms and classes, so which call fails depends on which group the
school happens to leave empty.

Observed payload (types only, values omitted):

dbi=dict(len=25, {classes:dict, classrooms:list, dayparts:dict, event_types:dict,
                  absent_types:list, ...})

Accounts at a school that populates every group are unaffected, which is why
this looks intermittent.

Fix

Normalise array-shaped groups in __get_item_group() — the single place every
group lookup passes through:

item_group = dbi.get(item_group_name)

if isinstance(item_group, list):
    # An item group with no entries is serialised by EduPage as an empty
    # JSON array instead of an empty object. Callers rely on mapping
    # access (see __get_item_with_id), so normalise arrays into a
    # mapping keyed by position to keep them working.
    return {str(index): entry for index, entry in enumerate(item_group)}

return item_group

An empty array becomes an empty mapping, which is exactly the pre-change
behaviour. Groups that are already objects are returned untouched, and a
missing group or missing dbi still returns None.

This is deliberately handled generically rather than per group name: the shape
change is not tied to one group, and EduPage may serialise other groups this
way later.

Tests

New tests/test_dbi.py, following the existing unittest conventions:

  • empty array group is read as an empty mapping, and id lookups return None
  • every id-lookup group (teachers, students, subjects, classrooms,
    classes) is normalised, not just one
  • an object group is returned unchanged
  • a missing group and a missing dbi still return None

Verified with the full suite: 38 passed (33 pre-existing + 5 new). Without
the patch, 2 of the new tests fail.

$ python -m unittest discover -s tests
Ran 38 tests in 0.063s
OK

Notes for review

  • Non-empty arrays are normalised to positional string keys. I have not observed
    a non-empty array in a dbi group, so that path is defensive rather than
    evidence-driven. If EduPage does send populated arrays, the key format should
    probably follow the item's own id instead of its position — happy to adjust
    if anyone has a sample payload.
  • No behaviour change for well-formed payloads.

Which group is actually empty

Worth recording, because it is not obvious and it explains why this looks
intermittent. The failing lookup reaches __get_item_with_id() via

get_teacher() -> get_teachers() -> EduAccount.parse -> fetch_classroom_number()

so the group left empty is classrooms, teachers or subjects. An empty
classes group fails differently - with 'list' object has no attribute 'items' - because get_classes() iterates it rather than calling .get() on it.
Reproduced against 0.13.0:

get_classes w/ classrooms=[]  -> AttributeError: 'list' object has no attribute 'get'
fetch_teacher_data w/ teachers=[] -> AttributeError: 'list' object has no attribute 'get'
get_classes w/ classes=[]     -> AttributeError: 'list' object has no attribute 'items'

Different schools leave different groups empty, which is why only some accounts
are affected.

Related

  • Reported via rine77/homeassistantedupage (the Home Assistant integration),
    where it presents as the same AttributeError on edupage-api 0.13.0/0.13.1.
  • eduPage account used for testing is a parent account; the failing account is
    at a different school whose relevant group is empty.

EduPage serialises a `dbi` item group that has no entries as an empty JSON array
instead of an empty object. `DbiHelper.__get_item_with_id()` calls `.get()` on
the group, so an array-shaped group raises:

    AttributeError: 'list' object has no attribute 'get'

Read an *empty* array as an empty mapping:

    if isinstance(item_group, list) and not item_group:
        return {}

Groups that are already objects are returned unchanged, and a missing group or a
missing `dbi` still returns `None`.

Only empty arrays are handled. A populated array has not been observed in a `dbi`
group, and mapping it onto positional keys would fabricate ids that can never
match a real reference - so a populated array is left alone and keeps failing
loudly rather than silently losing data.

Reported via rine77/homeassistantedupage (the Home Assistant integration), where
it presents as get_classes() and get_timetable() failing every poll:

    get_classes failed: 'list' object has no attribute 'get'
    Failed to fetch timetable data for <date>: 'list' object has no attribute 'get'

The failing lookup reaches this through get_teacher() -> get_teachers() ->
EduAccount.parse -> fetch_classroom_number(), so the group left empty is
classrooms, teachers or subjects. An empty `classes` group fails differently -
with 'list' object has no attribute 'items' - because get_classes() iterates it
rather than calling .get() on it.
@Koky05
Koky05 force-pushed the fix-dbi-array-item-groups branch from 58b0585 to d2f3e63 Compare October 8, 2026 18:48
Koky05 added a commit to Koky05/homeassistantedupage that referenced this pull request Oct 8, 2026
Two independent fixes for 2026.10.0, plus regression tests.

1. Empty array-shaped `dbi` item groups

EduPage serialises a `dbi` item group that has no entries as an empty JSON array
instead of an empty object. `DbiHelper.__get_item_with_id()` calls `.get()` on
the group, so an array-shaped group raises:

    AttributeError: 'list' object has no attribute 'get'

breaking get_classes() and get_timetable() on every poll for accounts at schools
that leave a group empty. The timetable loop breaks on the first failure, so the
whole 14-day window is lost and the calendar stays empty.

`_normalise_dbi_groups()` reads an *empty* array as an empty mapping, applied at
the two points where a payload is handed to the library:

- `Edupage._load_session()` - the runtime path used on setup and every poll
- `config_flow._finalize_setup()` - every initial-setup path converges here:
  plain login, the official two-factor flow, and the custom fallback driver

A populated array is deliberately left alone: mapping it onto positional keys
would fabricate ids that can never match a real reference, so it keeps raising
instead of silently losing data. Nested arrays inside a group are untouched too.

The reconfigure path (`_apply_reconfigure`) needs no change - it reads only the
PHPSESSID cookie and never touches `dbi`, and the reload it triggers
re-normalises via `_load_session()`.

Tests: `tests/test_dbi_normalise.py` covers empty groups, unchanged dictionaries
(identity-checked), nested arrays left alone, populated arrays untouched,
missing/malformed payloads, idempotence, the runtime call site, and an
end-to-end check that the real library no longer raises.

2. `await entry.async_start_reauth(...)`

`async_start_reauth()` is a `@callback` annotated `-> None` that schedules its own
task, so awaiting it always raises `TypeError: 'NoneType' object can't be
awaited`. Both call sites did that, on the session-expired path, so the reauth
flow was never started and the entry could not be recovered from the UI.

This is kept as its own commit so it can be reviewed or reverted independently.

Upstream fix for the library-side bug: EdupageAPI/edupage-api#126. This
workaround can be dropped once a released edupage-api includes it.
Koky05 added a commit to Koky05/homeassistantedupage that referenced this pull request Oct 8, 2026
Two independent fixes for 2026.10.0, plus regression tests.

1. Empty array-shaped `dbi` item groups

EduPage serialises a `dbi` item group that has no entries as an empty JSON array
instead of an empty object. `DbiHelper.__get_item_with_id()` calls `.get()` on
the group, so an array-shaped group raises:

    AttributeError: 'list' object has no attribute 'get'

breaking get_classes() and get_timetable() on every poll for accounts at schools
that leave a group empty. The timetable loop breaks on the first failure, so the
whole 14-day window is lost and the calendar stays empty.

`_normalise_dbi_groups()` reads an *empty* array as an empty mapping, applied at
the two points where a payload is handed to the library:

- `Edupage._load_session()` - the runtime path used on setup and every poll
- `config_flow._finalize_setup()` - every initial-setup path converges here:
  plain login, the official two-factor flow, and the custom fallback driver

A populated array is deliberately left alone: mapping it onto positional keys
would fabricate ids that can never match a real reference, so it keeps raising
instead of silently losing data. Nested arrays inside a group are untouched too.

The reconfigure path (`_apply_reconfigure`) needs no change - it reads only the
PHPSESSID cookie and never touches `dbi`, and the reload it triggers
re-normalises via `_load_session()`.

Tests: `tests/test_dbi_normalise.py` covers empty groups, unchanged dictionaries
(identity-checked), nested arrays left alone, populated arrays untouched,
missing/malformed payloads, idempotence, the runtime call site, and an
end-to-end check that the real library no longer raises.

2. `await entry.async_start_reauth(...)`

`async_start_reauth()` is a `@callback` annotated `-> None` that schedules its own
task, so awaiting it always raises `TypeError: 'NoneType' object can't be
awaited`. Both call sites did that, on the session-expired path, so the reauth
flow was never started and the entry could not be recovered from the UI.

This is kept as its own commit so it can be reviewed or reverted independently.

Upstream fix for the library-side bug: EdupageAPI/edupage-api#126. This
workaround can be dropped once a released edupage-api includes it.

`tests/test_reauth.py` is updated accordingly: it used `AsyncMock` for
`entry.async_start_reauth` and asserted `assert_awaited_once()`, which encoded
the bug rather than the intended behaviour. It now uses `Mock` and asserts on the
call. No other test asserted the awaited form - the remaining `assert_awaited*`
uses in the suite are `async_call_service` and `config_entries.async_reload`,
which are genuine coroutines.
rine77 pushed a commit to rine77/homeassistantedupage that referenced this pull request Oct 9, 2026
Two independent fixes for 2026.10.0, plus regression tests.

1. Empty array-shaped `dbi` item groups

EduPage serialises a `dbi` item group that has no entries as an empty JSON array
instead of an empty object. `DbiHelper.__get_item_with_id()` calls `.get()` on
the group, so an array-shaped group raises:

    AttributeError: 'list' object has no attribute 'get'

breaking get_classes() and get_timetable() on every poll for accounts at schools
that leave a group empty. The timetable loop breaks on the first failure, so the
whole 14-day window is lost and the calendar stays empty.

`_normalise_dbi_groups()` reads an *empty* array as an empty mapping, applied at
the two points where a payload is handed to the library:

- `Edupage._load_session()` - the runtime path used on setup and every poll
- `config_flow._finalize_setup()` - every initial-setup path converges here:
  plain login, the official two-factor flow, and the custom fallback driver

A populated array is deliberately left alone: mapping it onto positional keys
would fabricate ids that can never match a real reference, so it keeps raising
instead of silently losing data. Nested arrays inside a group are untouched too.

The reconfigure path (`_apply_reconfigure`) needs no change - it reads only the
PHPSESSID cookie and never touches `dbi`, and the reload it triggers
re-normalises via `_load_session()`.

Tests: `tests/test_dbi_normalise.py` covers empty groups, unchanged dictionaries
(identity-checked), nested arrays left alone, populated arrays untouched,
missing/malformed payloads, idempotence, the runtime call site, and an
end-to-end check that the real library no longer raises.

2. `await entry.async_start_reauth(...)`

`async_start_reauth()` is a `@callback` annotated `-> None` that schedules its own
task, so awaiting it always raises `TypeError: 'NoneType' object can't be
awaited`. Both call sites did that, on the session-expired path, so the reauth
flow was never started and the entry could not be recovered from the UI.

This is kept as its own commit so it can be reviewed or reverted independently.

Upstream fix for the library-side bug: EdupageAPI/edupage-api#126. This
workaround can be dropped once a released edupage-api includes it.

`tests/test_reauth.py` is updated accordingly: it used `AsyncMock` for
`entry.async_start_reauth` and asserted `assert_awaited_once()`, which encoded
the bug rather than the intended behaviour. It now uses `Mock` and asserts on the
call. No other test asserted the awaited form - the remaining `assert_awaited*`
uses in the suite are `async_call_service` and `config_entries.async_reload`,
which are genuine coroutines.
@ivanhrabcak

Copy link
Copy Markdown
Member

I thought about what to do when edupage serializes something as a non-empty array. I've decided that can't happen and if it will, other things will probably break, so leaving it to fail with AttributeError is probably correct.

Thank you for your contribution!

@ivanhrabcak
ivanhrabcak merged commit d0a075b into EdupageAPI:master Oct 11, 2026
2 checks passed
Koky05 added a commit to Koky05/edupage-api that referenced this pull request Oct 12, 2026
The body of `if item_group == []:` uses 3 spaces instead of 12.

    if item_group == []:
       return {}

Shipped in 0.13.2 via EdupageAPI#126. No behaviour change.
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.

2 participants