#๐Ÿ”’ FastApi Type App.State "not subscriptable"

89 messages ยท Page 1 of 1 (latest)

lucid lotus
#

state["config"].username can't be accessed, I get the errror:

   correct_username_bytes = state["config"].username.encode("utf8")
TypeError: 'State' object is not subscriptable

Why is that?

config = AppConfig()

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[AppState]:
    """Lifespan event executed before the application starts handling requests."""
    yield {
        "config": config,
        "rabbitmq": RabbitMQ(config),
    }


app = FastAPI(lifespan=lifespan)

app.include_router(customers.router, dependencies=[Depends(validate_auth)])
app.include_router(mails.router, dependencies=[Depends(validate_auth)])
class AppState(TypedDict):
    """Model that represents FastApi app.state."""

    config: AppConfig
    rabbitmq: RabbitMQ
if TYPE_CHECKING:
    from misc.app_state import AppState

security = HTTPBasic()


def validate_auth(
    request: Request,
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> None:
    """Enforces user authentication."""
    state = cast("AppState", request.state)
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = state["config"].username.encode("utf8")
fiery walrusBOT
#

@lucid lotus

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

lucid lotus
#

pushhh

cinder depot
lucid lotus
#

you mean without .username?

cinder depot
#

state.config.username.encode....

#

i think state behaves like an object and not like a dictionary

lucid lotus
#

the typing doesn't allow this because AppState is a typedDict

#

but when I access it untyped like this request.state.config.username.encode("utf8") it does work

cinder depot
lucid lotus
#

not possible becasue lifespan expects to yield a dict

#

but then the state cannot be accessed like a dict

cinder depot
#

update the lifespan

lucid lotus
#

how?

cinder depot
#

directly yield Appstate(.....) object

lucid lotus
#

I'm a bit confused, why does it make a difference?

cinder depot
#

yeild in lifespan doesnt have to return a dict

#

it should be able to return any object

#

so if u yield custom appstate object then request.state will be that object

lucid lotus
#
class AppState(TypedDict):
    """Model that represents FastApi app.state."""

    config: AppConfig
    rabbitmq: RabbitMQ

so I can make this to a dataclass instead and remove the TypedDict?

cinder depot
#

yes

#

that way you can access the class variables

lucid lotus
#

when I do this I will get an type error here;
app = FastAPI(lifespan=lifespan)

saying:

Argument of type "(_: FastAPI) -> _AsyncGeneratorContextManager[AppState, None]" cannot be assigned to parameter "lifespan" of type "StatelessLifespan[FastAPI] | StatefulLifespan[FastAPI] | None" in function "__init__"
  Type "(_: FastAPI) -> _AsyncGeneratorContextManager[AppState, None]" is not assignable to type "StatelessLifespan[FastAPI] | StatefulLifespan[FastAPI] | None"
    Type "(_: FastAPI) -> _AsyncGeneratorContextManager[AppState, None]" is not assignable to type "StatelessLifespan[FastAPI]"
      Function return type "_AsyncGeneratorContextManager[AppState, None]" is incompatible with type "AbstractAsyncContextManager[None, bool | None]"
        "_AsyncGeneratorContextManager[AppState, None]" is not assignable to "AbstractAsyncContextManager[None, bool | None]"
          Type parameter "_T_co@AbstractAsyncContextManager" is covariant, but "AppState" is not a subtype of "None"
    Type "(_: FastAPI) -> _AsyncGeneratorContextManager[AppState, None]" is not assignable to type "StatefulLifespan[FastAPI]"
cinder depot
#

also use -> AsyncIterator[None]:

#

since youll be returning None, but can still acess app.state.app_state_obj

lucid lotus
#

Well something like this is not possible
app.state = AppState(config=config, rabbitmq=RabbitMQ(config))

Cannot assign to attribute "state" for class "FastAPI"
  "AppState" is not assignable to "State

and doing this would be partially untyped again:
app.state.test = AppState(config=config, rabbitmq=RabbitMQ(config))

cinder depot
lucid lotus
#

no worries thanks for having a look!

cinder depot
#

could you try returning AsyncIterator[dict[str, Any]] and then yield normally as you did in the original one, but define AppState as a dataclass?

#

youll need some changes to the validate auth if this works

lucid lotus
#

This

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, Any]]:
    """Lifespan event executed before the application starts handling requests."""
    # state is currently not typed, careful with changes here
    yield
    {
        "config": config,
        "rabbitmq": RabbitMQ(config),
    }

gives a typing error for the yield because types don't match

#

Current app state is not anymore a dict

@dataclass(kw_only=True)
class AppState:
    """Model that represents FastApi app.state."""

    config: AppConfig
    rabbitmq: RabbitMQ
cinder depot
lucid lotus
#

like what?

cinder depot
#

or otherwise it will yield None

cinder depot
lucid lotus
#

oh damn

#

still Return type, "AsyncIterator[dict[str, Unknown]]", is partially unknownPylancereportUnknownParameterType

cinder depot
#

dang these new errors

lucid lotus
#

I start to believe that maybe it's really not possible to type this thing

#

lifespan has to be one of those:

StatelessLifespan = Callable[[AppType], AbstractAsyncContextManager[None]]
StatefulLifespan = Callable[[AppType], AbstractAsyncContextManager[Mapping[str, Any]]]
Lifespan = Union[StatelessLifespan[AppType], StatefulLifespan[AppType]]
#

so it explicitly asks for a dict if not None

#

but then you don't access it in state like a dict makes no sense

cinder depot
lucid lotus
#

ah wait stupid me didn't import any lol

cinder depot
#

oh ๐Ÿ’€

lucid lotus
#

This works of course because it's a dict:

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, Any]]:
    """Lifespan event executed before the application starts handling requests."""
    # state is currently not typed, careful with changes here

    yield {
        "config": config,
        "rabbitmq": RabbitMQ(config),
    }
#

shame on me

#

but it's untyped ^^

cinder depot
#

well uh thats true u lost type safety in there to make it work

lucid lotus
#

With this;

def validate_auth(
    request: Request,
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> None:
    """Enforces user authentication."""
    state = cast("AppState", request.state)
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = state.config.username.encode("utf8")
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = state.config.password.encode("utf8")
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )

It does work but it's like only half typed ^^

#

Perhaps I create the object and convert it to a dict and yield that

cinder depot
#

back to the same point

#

there might be a way to ensure type safety by using fast apis Depends() feature....

lucid lotus
# cinder depot back to the same point

Like this will be type safe, but such a weird solution:

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, Any]]:
    """Lifespan event executed before the application starts handling requests."""
    # state is currently not typed, careful with changes here
    state = AppState(config=config, rabbitmq=RabbitMQ(config))
    yield state.to_dict()
cinder depot
lucid lotus
#

I do really think though this is bad implementation from fastapi. If lifespan expects to yield a dict I would expect to be able to access it like a dict

lucid lotus
cinder depot
cinder depot
lucid lotus
#

Doing it now like this with pydantic:

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, Any]]:
    """Lifespan event executed before the application starts handling requests."""
    # state is currently not typed, careful with changes here
    state = AppState(config=config, rabbitmq=RabbitMQ(config))
    yield state.model_dump()
class AppState(BaseModel):
    """Model that represents FastApi app.state."""

    config: AppConfig
    rabbitmq: RabbitMQ
lucid lotus
#

Hey thanks for taking the time to have a look!

cinder depot
cinder depot
lucid lotus
#

but with a custom to_dict it works

cinder depot
lucid lotus
#

I think it's fine like that not optimal but best I was able to come up within a reasonable time frame

#

I'll make an issue for later to have a look again if I have some time

cinder depot
#

is this for a personal project

lucid lotus
#

paying customer ^^

#

freelance stuff

cinder depot
#

oh damn

lucid lotus
#

hahaha

#

it is what it is

cinder depot
#

good luck the customer wont care about the efficiency part probs right now focus on making it all work

#

returning to it later if u have time sounds good

lucid lotus
#

it's really not a concern, will be an internal tool for a car garage

#

they are more concerned with things working correctly then being efficient

cinder depot
#

haha true

lucid lotus
#

they forced me to use python so that's actually first project for me I'm doing in this language

#

they know the risk ^^

#

!close

fiery walrusBOT
#
Python help channel closed with !close

This help channel has been closed. Feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.