Skip to content

Commit 6b67391

Browse files
committed
fix: strong and consistent typing across the library
BREAKING CHANGE: this change introduces a very strong typing across the library. From now on, we demand that each method returns a single type and converts the results into the specified return type, otherwise it raises a TypeError exception. This should resolve any type checking linter errors within the library as well.
1 parent 30a1367 commit 6b67391

18 files changed

Lines changed: 5185 additions & 1337 deletions

poetry.lock

Lines changed: 141 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/keycloak/authorization/permission.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def logic(self) -> str:
133133
return self._logic
134134

135135
@logic.setter
136-
def logic(self, value: str) -> str:
136+
def logic(self, value: str) -> None:
137137
self._logic = value
138138

139139
@property

src/keycloak/authorization/policy.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
from keycloak.exceptions import KeycloakAuthorizationConfigError
2626

27+
from .permission import Permission
28+
from .role import Role
29+
2730

2831
class Policy:
2932
"""
@@ -172,7 +175,7 @@ def permissions(self) -> list:
172175
def permissions(self, value: list) -> None:
173176
self._permissions = value
174177

175-
def add_role(self, role: dict) -> None:
178+
def add_role(self, role: str | Role) -> None:
176179
"""
177180
Add keycloak role in policy.
178181
@@ -185,7 +188,7 @@ def add_role(self, role: dict) -> None:
185188
raise KeycloakAuthorizationConfigError(error_msg)
186189
self._roles.append(role)
187190

188-
def add_permission(self, permission: dict) -> None:
191+
def add_permission(self, permission: str | Permission) -> None:
189192
"""
190193
Add keycloak permission in policy.
191194

src/keycloak/authorization/role.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_name(self) -> str:
6161
"""
6262
return self.name
6363

64-
def __eq__(self, other: str | Role) -> bool:
64+
def __eq__(self, other: object) -> bool:
6565
"""
6666
Eq method.
6767

src/keycloak/connection.py

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@
2727
try:
2828
from urllib.parse import urljoin
2929
except ImportError: # pragma: no cover
30-
from urlparse import urljoin
30+
from urlparse import urljoin # pyright: ignore[reportMissingImports]
31+
32+
from typing import Any
3133

3234
import httpx
3335
import requests
3436
from httpx import Response as AsyncResponse
3537
from requests import Response
3638
from requests.adapters import HTTPAdapter
39+
from requests_toolbelt import MultipartEncoder
3740

3841
from .exceptions import KeycloakConnectionError
3942

@@ -67,8 +70,8 @@ def __init__(
6770
self,
6871
base_url: str,
6972
headers: dict | None = None,
70-
timeout: int = 60,
71-
verify: bool = True,
73+
timeout: int | None = 60,
74+
verify: bool | str = True,
7275
proxies: dict | None = None,
7376
cert: str | tuple | None = None,
7477
max_retries: int = 1,
@@ -112,9 +115,13 @@ def __init__(
112115
adapter_kwargs = {"max_retries": max_retries}
113116
if pool_maxsize is not None:
114117
adapter_kwargs["pool_maxsize"] = pool_maxsize
115-
adapter = HTTPAdapter(**adapter_kwargs)
118+
adapter = HTTPAdapter(**adapter_kwargs) # pyright: ignore[reportArgumentType]
116119
# adds POST to retry whitelist
117-
allowed_methods = set(adapter.max_retries.allowed_methods)
120+
allowed_methods = (
121+
set(adapter.max_retries.allowed_methods)
122+
if adapter.max_retries.allowed_methods
123+
else set()
124+
)
118125
allowed_methods.add("POST")
119126
adapter.max_retries.allowed_methods = frozenset(allowed_methods)
120127

@@ -132,8 +139,8 @@ def __init__(
132139
max_keepalive_connections=20,
133140
),
134141
)
135-
self.async_s.auth = None # don't let requests add auth headers
136-
self.async_s.transport = httpx.AsyncHTTPTransport(retries=1)
142+
self.async_s.auth = None # pyright: ignore[reportAttributeAccessIssue]
143+
self.async_s.transport = httpx.AsyncHTTPTransport(retries=1) # pyright: ignore[reportAttributeAccessIssue]
137144

138145
async def aclose(self) -> None:
139146
"""Close the async connection on delete."""
@@ -146,7 +153,7 @@ def __del__(self) -> None:
146153
self._s.close()
147154

148155
@property
149-
def base_url(self) -> str:
156+
def base_url(self) -> str | None:
150157
"""
151158
Return base url in use for requests to the server.
152159
@@ -156,11 +163,11 @@ def base_url(self) -> str:
156163
return self._base_url
157164

158165
@base_url.setter
159-
def base_url(self, value: str) -> None:
166+
def base_url(self, value: str | None) -> None:
160167
self._base_url = value
161168

162169
@property
163-
def timeout(self) -> int:
170+
def timeout(self) -> int | None:
164171
"""
165172
Return timeout in use for request to the server.
166173
@@ -170,11 +177,11 @@ def timeout(self) -> int:
170177
return self._timeout
171178

172179
@timeout.setter
173-
def timeout(self, value: int) -> None:
180+
def timeout(self, value: int | None) -> None:
174181
self._timeout = value
175182

176183
@property
177-
def verify(self) -> bool:
184+
def verify(self) -> bool | str:
178185
"""
179186
Return verify in use for request to the server.
180187
@@ -184,11 +191,11 @@ def verify(self) -> bool:
184191
return self._verify
185192

186193
@verify.setter
187-
def verify(self, value: bool) -> None:
194+
def verify(self, value: bool | str) -> None:
188195
self._verify = value
189196

190197
@property
191-
def cert(self) -> str | tuple:
198+
def cert(self) -> str | tuple | None:
192199
"""
193200
Return client certificates in use for request to the server.
194201
@@ -198,7 +205,7 @@ def cert(self) -> str | tuple:
198205
return self._cert
199206

200207
@cert.setter
201-
def cert(self, value: str | tuple) -> None:
208+
def cert(self, value: str | tuple | None) -> None:
202209
self._cert = value
203210

204211
@property
@@ -216,7 +223,7 @@ def pool_maxsize(self, value: int | None) -> None:
216223
self._pool_maxsize = value
217224

218225
@property
219-
def headers(self) -> dict:
226+
def headers(self) -> dict | None:
220227
"""
221228
Return header request to the server.
222229
@@ -226,7 +233,7 @@ def headers(self) -> dict:
226233
return self._headers
227234

228235
@headers.setter
229-
def headers(self, value: dict) -> None:
236+
def headers(self, value: dict | None) -> None:
230237
self._headers = value or {}
231238

232239
def param_headers(self, key: str) -> str | None:
@@ -238,7 +245,7 @@ def param_headers(self, key: str) -> str | None:
238245
:returns: If the header parameters exist, return its value.
239246
:rtype: str
240247
"""
241-
return self.headers.get(key)
248+
return (self.headers or {}).get(key)
242249

243250
def clean_headers(self) -> None:
244251
"""Clear header parameters."""
@@ -264,6 +271,9 @@ def add_param_headers(self, key: str, value: str) -> None:
264271
:param value: Value to be added.
265272
:type value: str
266273
"""
274+
if self.headers is None:
275+
self.headers = {}
276+
267277
self.headers[key] = value
268278

269279
def del_param_headers(self, key: str) -> None:
@@ -273,9 +283,12 @@ def del_param_headers(self, key: str) -> None:
273283
:param key: Key of the header parameters.
274284
:type key: str
275285
"""
286+
if self.headers is None:
287+
return
288+
276289
self.headers.pop(key, None)
277290

278-
def raw_get(self, path: str, **kwargs: dict) -> Response:
291+
def raw_get(self, path: str, **kwargs: Any) -> Response: # noqa: ANN401
279292
"""
280293
Submit get request to the path.
281294
@@ -287,6 +300,9 @@ def raw_get(self, path: str, **kwargs: dict) -> Response:
287300
:rtype: Response
288301
:raises KeycloakConnectionError: HttpError Can't connect to server.
289302
"""
303+
if self.base_url is None:
304+
msg = "Unable to perform GET call with base_url missing."
305+
raise AttributeError(msg)
290306
try:
291307
return self._s.get(
292308
urljoin(self.base_url, path),
@@ -300,7 +316,7 @@ def raw_get(self, path: str, **kwargs: dict) -> Response:
300316
msg = "Can't connect to server"
301317
raise KeycloakConnectionError(msg) from e
302318

303-
def raw_post(self, path: str, data: dict, **kwargs: dict) -> Response:
319+
def raw_post(self, path: str, data: dict | str | MultipartEncoder, **kwargs: Any) -> Response: # noqa: ANN401
304320
"""
305321
Submit post request to the path.
306322
@@ -314,6 +330,9 @@ def raw_post(self, path: str, data: dict, **kwargs: dict) -> Response:
314330
:rtype: Response
315331
:raises KeycloakConnectionError: HttpError Can't connect to server.
316332
"""
333+
if self.base_url is None:
334+
msg = "Unable to perform POST call with base_url missing."
335+
raise AttributeError(msg)
317336
try:
318337
return self._s.post(
319338
urljoin(self.base_url, path),
@@ -328,7 +347,7 @@ def raw_post(self, path: str, data: dict, **kwargs: dict) -> Response:
328347
msg = "Can't connect to server"
329348
raise KeycloakConnectionError(msg) from e
330349

331-
def raw_put(self, path: str, data: dict, **kwargs: dict) -> Response:
350+
def raw_put(self, path: str, data: dict | str, **kwargs: Any) -> Response: # noqa: ANN401
332351
"""
333352
Submit put request to the path.
334353
@@ -342,6 +361,10 @@ def raw_put(self, path: str, data: dict, **kwargs: dict) -> Response:
342361
:rtype: Response
343362
:raises KeycloakConnectionError: HttpError Can't connect to server.
344363
"""
364+
if self.base_url is None:
365+
msg = "Unable to perform PUT call with base_url missing."
366+
raise AttributeError(msg)
367+
345368
try:
346369
return self._s.put(
347370
urljoin(self.base_url, path),
@@ -356,7 +379,7 @@ def raw_put(self, path: str, data: dict, **kwargs: dict) -> Response:
356379
msg = "Can't connect to server"
357380
raise KeycloakConnectionError(msg) from e
358381

359-
def raw_delete(self, path: str, data: dict | None = None, **kwargs: dict) -> Response:
382+
def raw_delete(self, path: str, data: dict | None = None, **kwargs: Any) -> Response: # noqa: ANN401
360383
"""
361384
Submit delete request to the path.
362385
@@ -370,6 +393,10 @@ def raw_delete(self, path: str, data: dict | None = None, **kwargs: dict) -> Res
370393
:rtype: Response
371394
:raises KeycloakConnectionError: HttpError Can't connect to server.
372395
"""
396+
if self.base_url is None:
397+
msg = "Unable to perform DELETE call with base_url missing."
398+
raise AttributeError(msg)
399+
373400
try:
374401
return self._s.delete(
375402
urljoin(self.base_url, path),
@@ -384,7 +411,7 @@ def raw_delete(self, path: str, data: dict | None = None, **kwargs: dict) -> Res
384411
msg = "Can't connect to server"
385412
raise KeycloakConnectionError(msg) from e
386413

387-
async def a_raw_get(self, path: str, **kwargs: dict) -> AsyncResponse:
414+
async def a_raw_get(self, path: str, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
388415
"""
389416
Submit get request to the path.
390417
@@ -396,6 +423,10 @@ async def a_raw_get(self, path: str, **kwargs: dict) -> AsyncResponse:
396423
:rtype: Response
397424
:raises KeycloakConnectionError: HttpError Can't connect to server.
398425
"""
426+
if self.base_url is None:
427+
msg = "Unable to perform GET call with base_url missing."
428+
raise AttributeError(msg)
429+
399430
try:
400431
return await self.async_s.get(
401432
urljoin(self.base_url, path),
@@ -407,7 +438,12 @@ async def a_raw_get(self, path: str, **kwargs: dict) -> AsyncResponse:
407438
msg = "Can't connect to server"
408439
raise KeycloakConnectionError(msg) from e
409440

410-
async def a_raw_post(self, path: str, data: dict, **kwargs: dict) -> AsyncResponse:
441+
async def a_raw_post(
442+
self,
443+
path: str,
444+
data: dict | str | MultipartEncoder,
445+
**kwargs: Any, # noqa: ANN401
446+
) -> AsyncResponse:
411447
"""
412448
Submit post request to the path.
413449
@@ -421,6 +457,10 @@ async def a_raw_post(self, path: str, data: dict, **kwargs: dict) -> AsyncRespon
421457
:rtype: Response
422458
:raises KeycloakConnectionError: HttpError Can't connect to server.
423459
"""
460+
if self.base_url is None:
461+
msg = "Unable to perform POST call with base_url missing."
462+
raise AttributeError(msg)
463+
424464
try:
425465
return await self.async_s.request(
426466
method="POST",
@@ -434,7 +474,7 @@ async def a_raw_post(self, path: str, data: dict, **kwargs: dict) -> AsyncRespon
434474
msg = "Can't connect to server"
435475
raise KeycloakConnectionError(msg) from e
436476

437-
async def a_raw_put(self, path: str, data: dict, **kwargs: dict) -> AsyncResponse:
477+
async def a_raw_put(self, path: str, data: dict | str, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
438478
"""
439479
Submit put request to the path.
440480
@@ -448,6 +488,10 @@ async def a_raw_put(self, path: str, data: dict, **kwargs: dict) -> AsyncRespons
448488
:rtype: Response
449489
:raises KeycloakConnectionError: HttpError Can't connect to server.
450490
"""
491+
if self.base_url is None:
492+
msg = "Unable to perform PUT call with base_url missing."
493+
raise AttributeError(msg)
494+
451495
try:
452496
return await self.async_s.put(
453497
urljoin(self.base_url, path),
@@ -464,7 +508,7 @@ async def a_raw_delete(
464508
self,
465509
path: str,
466510
data: dict | None = None,
467-
**kwargs: dict,
511+
**kwargs: Any, # noqa: ANN401
468512
) -> AsyncResponse:
469513
"""
470514
Submit delete request to the path.
@@ -479,6 +523,10 @@ async def a_raw_delete(
479523
:rtype: Response
480524
:raises KeycloakConnectionError: HttpError Can't connect to server.
481525
"""
526+
if self.base_url is None:
527+
msg = "Unable to perform DELETE call with base_url missing."
528+
raise AttributeError(msg)
529+
482530
try:
483531
return await self.async_s.request(
484532
method="DELETE",
@@ -493,7 +541,7 @@ async def a_raw_delete(
493541
raise KeycloakConnectionError(msg) from e
494542

495543
@staticmethod
496-
def _prepare_httpx_request_content(data: dict | str | None) -> dict:
544+
def _prepare_httpx_request_content(data: dict | str | None | MultipartEncoder) -> dict:
497545
"""
498546
Create the correct request content kwarg to `httpx.AsyncClient.request()`.
499547
@@ -504,9 +552,13 @@ def _prepare_httpx_request_content(data: dict | str | None) -> dict:
504552
:returns: A dict mapping the correct kwarg to the request content
505553
:rtype: dict
506554
"""
555+
if isinstance(data, MultipartEncoder):
556+
return {"content": data.to_string()}
557+
507558
if isinstance(data, str):
508559
# Note: this could also accept bytes, Iterable[bytes], or AsyncIterable[bytes]
509560
return {"content": data}
561+
510562
return {"data": data}
511563

512564
@staticmethod

0 commit comments

Comments
 (0)