openapi-python-client icon indicating copy to clipboard operation
openapi-python-client copied to clipboard

Fails to parse api.tidsbanken.net API response with "ValueError: dictionary update sequence element #0 has length 1; 2 is required"

Open Talkless opened this issue 1 month ago • 0 comments

Describe the bug

I tired to use openapi-python-client to generate API wrapper for some Norwegian time tracking system (api.tidsbanken.net/developer.tidsbanken.net).

Some api call https://api.tidsbanken.net/ansatt/ansatt?%24select=Id&%24top=3 (ansatt = worker) returns:

b'{"@odata.context":"https://api.tidsbanken.net/ansatt/$metadata#Ansatt(Id)","value":[{"Id":1},{"Id":4},{"Id":5}]}'

But that results in error:

Traceback (most recent call last):
  File "/home/vincas/code/python/tidsbanken_test/tidsbanken_test.py", line 82, in <module>
    main()
  File "/home/vincas/code/python/tidsbanken_test/tidsbanken_test.py", line 77, in main
    emp = get_all_employees(client)  
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/tidsbanken_test.py", line 21, in get_all_employees
    return getansatt.sync(
           ^^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/ansatt-client/ansatt_client/api/ansatt/getansatt.py", line 184, in sync
    return sync_detailed(
           ^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/ansatt-client/ansatt_client/api/ansatt/getansatt.py", line 141, in sync_detailed
    return _build_response(client=client, response=response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/ansatt-client/ansatt_client/api/ansatt/getansatt.py", line 82, in _build_response
    parsed=_parse_response(client=client, response=response),
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/ansatt-client/ansatt_client/api/ansatt/getansatt.py", line 61, in _parse_response
    componentsschemas_get_ansatt_model_item = GetAnsattModelItem.from_dict(
                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vincas/code/python/tidsbanken_test/ansatt-client/ansatt_client/models/get_ansatt_model_item.py", line 295, in from_dict
    d = dict(src_dict)
        ^^^^^^^^^^^^^^
ValueError: dictionary update sequence element #0 has length 1; 2 is required

OpenAPI Spec File

https://gist.github.com/Talkless/6199fbb4b0b4f62d0b2da6d4c78ec344

Desktop (please complete the following information):

  • OS: Debian 12 amd64
  • Python Version: 3.11
  • openapi-python-client version 0.27.1

Additional context

ChatGPT suggested to patch def _parse_response in ansatt-client/ansatt_client/api/ansatt/getansatt.py into:

def _parse_response(
    *, client: AuthenticatedClient | Client, response: httpx.Response
) -> list[GetAnsattModelItem] | None:
    if response.status_code == 200:
        data = response.json()

        # OData always wraps results inside "value"
        if isinstance(data, dict) and "value" in data:
            items = data["value"]
        else:
            items = data

        result = []

        for raw in items:
            # raw is a dict like {"Id": 1}
            result.append(GetAnsattModelItem.from_dict(raw))

        return result

    if client.raise_on_unexpected_status:
        raise errors.UnexpectedStatus(response.status_code, response.content)

    return None

And then it starts working.

Now I get list of GetAnsattModelItem (with just Id as requested):

[GetAnsattModelItem(id=1, fornavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, etternavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, adresse=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, postnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, poststed=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tlf_privat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, mobil=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, epost=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fodt=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tittel=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, lonnskonto=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, avdeling_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, aktiv=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, identifikator=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, notat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, prosjekt_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, stillingsprosent=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_1_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_2_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kjonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kommune=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_gruppe_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, innleie=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fastlonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_navn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_nr=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personalia_bekreftet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_halve_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_status_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_fra_lonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, filnavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ekstern_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, timelonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, additional_properties={}), GetAnsattModelItem(id=4, fornavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, etternavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, adresse=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, postnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, poststed=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tlf_privat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, mobil=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, epost=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fodt=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tittel=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, lonnskonto=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, avdeling_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, aktiv=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, identifikator=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, notat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, prosjekt_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, stillingsprosent=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_1_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_2_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kjonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kommune=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_gruppe_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, innleie=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fastlonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_navn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_nr=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personalia_bekreftet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_halve_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_status_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_fra_lonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, filnavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ekstern_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, timelonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, additional_properties={}), GetAnsattModelItem(id=5, fornavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, etternavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, adresse=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, postnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, poststed=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tlf_privat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, mobil=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, epost=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fodt=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, tittel=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, lonnskonto=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personnummer=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sluttet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, avdeling_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, aktiv=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, identifikator=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, notat=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, prosjekt_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, stillingsprosent=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_1_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, element_2_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kjonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, kommune=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ansatt_gruppe_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, innleie=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, fastlonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_navn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ice_nr=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, personalia_bekreftet=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, antall_halve_ferie_dager=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_status_rapporter=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, utelat_fra_lonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, filnavn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, ekstern_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, timelonn=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, sist_endret_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_dato=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, opprettet_av_id=<ansatt_client.types.Unset object at 0x7f33deffe0d0>, additional_properties={})]

So in the end I am not sure what to make of it:

  • Does tidsbanken api returns result in non-conforming format?
  • Is .yaml specification wrong so that openapi-python-client generates sub-optimal code?
  • Or it's just some bug in openapi-python-client?

Thanks!

My example application:

from pathlib import Path
from typeguard import typechecked
import httpx

import sys

# Add the folder that contains ansatt_client/
sys.path.append(str(Path(__file__).parent / "ansatt-client"))

from ansatt_client import Client
from ansatt_client.api.ansatt import getansatt

class Consts:
    TB_KEY = "redacted" # "API key"
    SUBSCRIPTION_KEY = "redacted"

@typechecked
def get_all_employees(client: Client):
    return getansatt.sync(
    client=client,
    select="Id",
    top=3,
    tb_key = Consts.TB_KEY)
    
    
def log_request(request):
    print(f"Request event hook: {request.method} {request.url} - Waiting for response")

def log_response(response: httpx.Response):
    request = response.request

    print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")

    print("---- HEADERS ----")
    for k, v in response.headers.items():
        print(f"{k}: {v}")
    print("---- END HEADERS ----")

    try:
        response.read()
    except Exception as e:
        print("ERROR during response.read():", e)

    print("---- RAW RESPONSE BODY ----")
    try:
        # Show raw for debugging JSON truncation
        print(response.content)
    except Exception as e:
        print("ERROR reading .content:", e)
    print("---- END RAW RESPONSE BODY ----")


def main():
    client = Client(
    base_url="https://api.tidsbanken.net/ansatt",
    timeout=30,
    headers={
        "tb-key": Consts.TB_KEY,
        "Ocp-Apim-Subscription-Key": Consts.SUBSCRIPTION_KEY,
        "x-api-version": "3.0",
        "Cache-Control": "no-cache",
    },
    httpx_args={
        "event_hooks": {
            "request": [log_request],
            "response": [log_response]
        }
    })
    
    emp = get_all_employees(client)  
    print(emp)
    
    
if __name__ == "__main__":
    main()


Talkless avatar Dec 04 '25 07:12 Talkless