I write a small declarative API client and I want it to be fully typed (query params, body, etc), especially when it comes to query_params and body I would like PyCharm to autosuggest what type the route expects when issuing the .call() method.
The user of this client is going to use it like following:
class ChuckApi(enum.StrEnum):
RANDOM_JOKE = "RANDOM_JOKE"
JOKE_CATEGORIES = "JOKE_CATEGORIES"
chuck_norris_api = HTTPXAPIClient(
client_config=APIClientConfig(
base_url="https://api.chucknorris.io",
),
api_routes=[
ApiRoute(
name=ChuckApi.RANDOM_JOKE,
url=URL("/jokes/random/"),
method=ApiMethod.GET,
success_typing=Joke,
fail_typing=Joke4XXResponse,
query_typing=JokeQP,
body_typing=JokeBody,
)
]
)
if __name__ == "__main__":
response = await chuck_norris_api.call(ChuckApi.RANDOM_JOKE)
ApiRoute is a Generic class that store route info:
ApiRouteQueryT = TypeVar("ApiRouteQueryT", bound=BaseModel)
ApiRouteBodyT = TypeVar("ApiRouteBodyT", bound=BaseModel)
SuccessResponseT = TypeVar("SuccessResponseT", bound=msgspec.Struct)
ErrorResponseT = TypeVar("ErrorResponseT", bound=msgspec.Struct)
class ApiRoute(Generic[ApiRouteQueryT, ApiRouteBodyT, SuccessResponseT, ErrorResponseT]):
def __init__(
self: Self,
*,
name: StrEnum,
url: URL,
method: ApiMethod,
success_typing: type[SuccessResponseT],
fail_typing: type[ErrorResponseT],
headers: Mapping[str, str] | None = None,
timeout: float | Timeout | None = None,
query_typing: type[ApiRouteQueryT] | None = None,
body_typing: type[ApiRouteBodyT] | None = None
):
self.url = self._validate_url(url)
self.name = name
self.method = method
self.success_typing = success_typing
self.fail_typing = fail_typing
self.headers = headers if headers is not None else {}
self.timeout = timeout
self.query_typing = query_typing
self.body_typing = body_typing
I store already declared ApiRoutes in a dictionary in my client instance:
self._api_route_registry: dict[str, ApiRoute[ApiRouteQueryT, ApiRouteBodyT, SuccessResponseT, ErrorResponseT]] = {}
The problem is that when I try to call endpoint by name I can’t hint the typings for query_param and body, because I need to retrieve the ApiRoute instance IN the body of the function and the function signature is STATIC –> no type hints in PyCharm.
async def call(
self: Self,
api_route_name: str,
*,
query_params: ApiRouteQueryT | None = None,
body: ApiRouteBodyT | None = None,
**kwargs: Any,
) -> ResponseWrapper[SuccessResponseT, ErrorResponseT]:
api_route: ApiRoute[ApiRouteQueryT, ApiRouteBodyT, SuccessResponseT, ErrorResponseT] = self.prepare_route(api_route_name, **kwargs)
I would like to know how to handle such cases and maybe there are workarounds that I do not see. I would appreciate any suggestion and answer because I am a little bit stuck. Thank you in advance.
I understand that PyCharm does not type hint it because I get it from dictionary in the function body