Skip to content

Commit 3166eae

Browse files
committed
refactor: Improve robustness and fix bugs in client library
1 parent 7e77da0 commit 3166eae

13 files changed

Lines changed: 66 additions & 31 deletions

VERSIONING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ This document provides a detailed explanation of the versioning and release stra
44

55
## Versioning at a Glance
66

7-
| Version Change | ZITADEL Compatibility | Type of Change | Example |
8-
| :--- | :--- | :--- | :--- |
9-
| **MAJOR** (e.g., 3.x -> 4.x) | Aligned with ZITADEL Core. Requires server upgrade. | Contains breaking changes. | `v3.5.1` -> `v4.0.0` |
10-
| **MINOR** (e.g., 3.1 -> 3.2) | Compatible with the same ZITADEL major version. | New, non-breaking features added. | `v3.1.4` -> `v3.2.0` |
11-
| **PATCH** (e.g., 3.1.0 -> 3.1.1)| Compatible with the same ZITADEL major version. | Backwards-compatible bug fixes. | `v3.1.0` -> `v3.1.1` |
7+
| Version Change | ZITADEL Compatibility | Type of Change | Example |
8+
|:---------------------------------|:----------------------------------------------------|:----------------------------------|:---------------------|
9+
| **MAJOR** (e.g., 3.x -> 4.x) | Aligned with ZITADEL Core. Requires server upgrade. | Contains breaking changes. | `v3.5.1` -> `v4.0.0` |
10+
| **MINOR** (e.g., 3.1 -> 3.2) | Compatible with the same ZITADEL major version. | New, non-breaking features added. | `v3.1.4` -> `v3.2.0` |
11+
| **PATCH** (e.g., 3.1.0 -> 3.1.1) | Compatible with the same ZITADEL major version. | Backwards-compatible bug fixes. | `v3.1.0` -> `v3.1.1` |
1212

1313
---
1414

devbox.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.7/.schema/devbox.schema.json",
33
"packages": [
4-
"python@latest",
54
"poetry@latest",
6-
"lefthook@latest"
5+
"lefthook@latest",
6+
"python39@latest"
77
],
88
"env": {
99
"POETRY_CACHE_DIR": "$PWD/.poetry"

devbox.lock

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,60 +114,60 @@
114114
}
115115
}
116116
},
117-
"python@latest": {
118-
"last_modified": "2025-03-11T17:52:14Z",
117+
"python39@latest": {
118+
"last_modified": "2025-03-21T17:37:26Z",
119119
"plugin_version": "0.0.3",
120-
"resolved": "github:NixOS/nixpkgs/0d534853a55b5d02a4ababa1d71921ce8f0aee4c#python313",
120+
"resolved": "github:NixOS/nixpkgs/94c4dbe77c0740ebba36c173672ca15a7926c993#python39",
121121
"source": "devbox-search",
122-
"version": "3.13.2",
122+
"version": "3.9.21",
123123
"systems": {
124124
"aarch64-darwin": {
125125
"outputs": [
126126
{
127127
"name": "out",
128-
"path": "/nix/store/c8k24sfwckindjhdxak6z3dhn8w4anx2-python3-3.13.2",
128+
"path": "/nix/store/nlbviyx8zris00ahxw4ddn69pc78x1zq-python3-3.9.21",
129129
"default": true
130130
}
131131
],
132-
"store_path": "/nix/store/c8k24sfwckindjhdxak6z3dhn8w4anx2-python3-3.13.2"
132+
"store_path": "/nix/store/nlbviyx8zris00ahxw4ddn69pc78x1zq-python3-3.9.21"
133133
},
134134
"aarch64-linux": {
135135
"outputs": [
136136
{
137137
"name": "out",
138-
"path": "/nix/store/jsqky488530ax8hdaz5ckldxq9y6qr3k-python3-3.13.2",
138+
"path": "/nix/store/d23m22zna540cijzviazc2jwc0ykxn0q-python3-3.9.21",
139139
"default": true
140140
},
141141
{
142142
"name": "debug",
143-
"path": "/nix/store/fwwjmw2yddxxm6aj51r16v0ffp77zvk1-python3-3.13.2-debug"
143+
"path": "/nix/store/xkdmi6r4v1r3s9h7ch02bly2pl34m61x-python3-3.9.21-debug"
144144
}
145145
],
146-
"store_path": "/nix/store/jsqky488530ax8hdaz5ckldxq9y6qr3k-python3-3.13.2"
146+
"store_path": "/nix/store/d23m22zna540cijzviazc2jwc0ykxn0q-python3-3.9.21"
147147
},
148148
"x86_64-darwin": {
149149
"outputs": [
150150
{
151151
"name": "out",
152-
"path": "/nix/store/wjxlppca1s14xzw5iz608qpywyl5mcdw-python3-3.13.2",
152+
"path": "/nix/store/za3lvl97h0nm46ziqdwdy8f3j6d7na26-python3-3.9.21",
153153
"default": true
154154
}
155155
],
156-
"store_path": "/nix/store/wjxlppca1s14xzw5iz608qpywyl5mcdw-python3-3.13.2"
156+
"store_path": "/nix/store/za3lvl97h0nm46ziqdwdy8f3j6d7na26-python3-3.9.21"
157157
},
158158
"x86_64-linux": {
159159
"outputs": [
160160
{
161161
"name": "out",
162-
"path": "/nix/store/njpnqszjaj6k38cp4466ygn74n392xy1-python3-3.13.2",
162+
"path": "/nix/store/ydq53zmlqq5wcn46w3h62p06yjg4xx8z-python3-3.9.21",
163163
"default": true
164164
},
165165
{
166166
"name": "debug",
167-
"path": "/nix/store/ixcc4jnj3byz0hg943wadfy1phxkmmgg-python3-3.13.2-debug"
167+
"path": "/nix/store/c3nr8wl0l1yy3lg9msa7453d3r7pp041-python3-3.9.21-debug"
168168
}
169169
],
170-
"store_path": "/nix/store/njpnqszjaj6k38cp4466ygn74n392xy1-python3-3.13.2"
170+
"store_path": "/nix/store/ydq53zmlqq5wcn46w3h62p06yjg4xx8z-python3-3.9.21"
171171
}
172172
}
173173
}

spec/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22
# mypy: allow-untyped-defs
33
from __future__ import annotations
44

5+
import sys
56
import os
67
import platform
78
import re
89

910
# noinspection PyPep8Naming
1011
import xml.etree.ElementTree as ET
1112
from collections import defaultdict
12-
from collections.abc import Callable
13+
14+
if sys.version_info >= (3, 10):
15+
# noinspection PyProtectedMember
16+
from collections import Callable # type: ignore
17+
else:
18+
from collections.abc import Callable
1319
from datetime import datetime, timezone
1420

1521
import pytest
@@ -30,6 +36,7 @@
3036
xml_key = StashKey["LogXML"]()
3137

3238

39+
# noinspection PyUnresolvedReferences
3340
class _NodeReporter:
3441
def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None:
3542
self.id = nodeid
@@ -299,6 +306,7 @@ def pytest_configure(config: Config) -> None:
299306
300307
:param config: The pytest Config object containing CLI options and hooks.
301308
"""
309+
# noinspection PyUnresolvedReferences
302310
xmldir = config.option.xmldir
303311
if xmldir and not hasattr(config, "workerinput"):
304312
config.stash[xml_key] = LogXML(xmldir)
@@ -333,6 +341,7 @@ def mangle_test_address(address: str) -> list[str]:
333341

334342

335343
class LogXML:
344+
# noinspection PyUnresolvedReferences
336345
def __init__( # type: ignore[no-untyped-def]
337346
self,
338347
output_dir,
@@ -350,6 +359,7 @@ def __init__( # type: ignore[no-untyped-def]
350359
self.log_passing_tests = log_passing_tests
351360
self.report_duration = report_duration
352361
self.stats: dict[str, int] = dict.fromkeys(["error", "passed", "failure", "skipped"], 0)
362+
# noinspection PyUnresolvedReferences
353363
self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {}
354364
self.node_reporters_ordered: list[_NodeReporter] = []
355365

@@ -370,6 +380,7 @@ def finalize(self, report: TestReport) -> None:
370380
reporter.finalize()
371381

372382
def node_reporter(self, report: TestReport | str) -> _NodeReporter:
383+
# noinspection PyUnresolvedReferences
373384
nodeid: str | TestReport = getattr(report, "nodeid", report)
374385
# Local hack to handle xdist report order.
375386
workernode = getattr(report, "node", None)

test/auth/test_oauth_authenticator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
import unittest
24

35
from testcontainers.core.container import DockerContainer
@@ -15,7 +17,7 @@ class OAuthAuthenticatorTest(unittest.TestCase):
1517
with a status code of 405, using HttpWaitStrategy.
1618
"""
1719

18-
oauth_host: str | None = None
20+
oauth_host: Optional[str] = None
1921
mock_oauth2_server: DockerContainer = None
2022

2123
@classmethod
@@ -24,6 +26,7 @@ def setup_class(cls) -> None:
2426
cls.mock_oauth2_server.start()
2527
host = cls.mock_oauth2_server.get_container_host_ip()
2628
port = cls.mock_oauth2_server.get_exposed_port(8080)
29+
# noinspection HttpUrlsUsage
2730
cls.oauth_host = f"http://{host}:{port}"
2831

2932
@classmethod

zitadel_client/api_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
self.default_headers[header_name] = header_value
7070
self.client_side_validation = configuration.client_side_validation
7171

72+
# noinspection PyArgumentList
7273
T = TypeVar("T", bound="ApiClient")
7374

7475
def __enter__(self: T) -> T:
@@ -229,8 +230,10 @@ def response_deserialize(
229230
response_type = response_types_map.get(str(response_data.status)[0] + "XX", None)
230231

231232
# deserialize response data
233+
# noinspection PyUnusedLocal
232234
response_text = None
233235
return_data = None
236+
# noinspection PyUnreachableCode
234237
try:
235238
if response_type == "bytearray":
236239
return_data = response_data.data
@@ -243,6 +246,7 @@ def response_deserialize(
243246
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
244247
encoding = match.group(1) if match else "utf-8"
245248
response_text = response_data.data.decode(encoding)
249+
# noinspection PyTypeChecker
246250
return_data = self.deserialize(response_text, response_type, content_type)
247251
finally:
248252
if not 200 <= response_data.status <= 299:
@@ -357,12 +361,14 @@ def __deserialize(data, klass): # noqa C901 too complex
357361
if klass.startswith("List["):
358362
m = re.match(r"List\[(.*)]", klass)
359363
assert m is not None, "Malformed List type definition"
364+
# noinspection PyArgumentList
360365
return [ApiClient.__deserialize(sub_data) for sub_data in data]
361366

362367
if klass.startswith("Dict["):
363368
m = re.match(r"Dict\[([^,]*), (.*)]", klass)
364369
assert m is not None, "Malformed Dict type definition"
365370
sub_kls = m.group(2)
371+
# noinspection PyArgumentList
366372
return {k: ApiClient.__deserialize(sub_kls) for k, v in data.items()}
367373

368374
# convert str to class

zitadel_client/api_response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
T = TypeVar("T")
88

99

10+
# noinspection PyTypeHints
1011
class ApiResponse(BaseModel, Generic[T]):
1112
"""
1213
API response object

zitadel_client/auth/authenticator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from datetime import datetime, timezone
2+
from datetime import datetime, timezone, timedelta
33
from typing import Any, Dict, Generic, TypeVar # noqa: F401
44

55

@@ -65,4 +65,4 @@ def is_expired(self) -> bool:
6565
Returns:
6666
- bool: True if expired, False otherwise.
6767
"""
68-
return datetime.now(timezone.utc) >= self.expires_at
68+
return datetime.now(timezone.utc) >= (self.expires_at - timedelta(minutes=5))

zitadel_client/auth/oauth_authenticator.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from threading import Lock
2+
13
from abc import ABC, abstractmethod
24
from datetime import datetime, timedelta, timezone
35
from typing import Any, Dict, Generic, Optional, TypeVar # noqa: F401
@@ -29,16 +31,20 @@ def __init__(self, open_id: OpenId, oauth_session: OAuth2Session):
2931
self.open_id = open_id
3032
self.token: Optional[Token] = None
3133
self.oauth_session = oauth_session
34+
self._lock = Lock()
3235

3336
def get_auth_token(self) -> str:
3437
"""
3538
Returns the current access token, refreshing it if necessary.
3639
"""
37-
if self.token is None or self.token.is_expired():
38-
self.refresh_token()
40+
with self._lock:
41+
if self.token is None or self.token.is_expired():
42+
self.refresh_token()
3943

40-
assert self.token is not None
41-
return self.token.access_token
44+
if self.token is None:
45+
raise ZitadelError("Token is null even after attempting to refresh.")
46+
else:
47+
return self.token.access_token
4248

4349
def get_auth_headers(self) -> Dict[str, str]:
4450
"""
@@ -75,10 +81,11 @@ def refresh_token(self) -> Token:
7581
raise ZitadelError("Failed to refresh token: " + str(e)) from e
7682

7783

84+
# noinspection PyArgumentList
7885
T = TypeVar("T", bound="OAuthAuthenticatorBuilder[Any]")
7986

8087

81-
# noinspection PyTypeHintsInspection
88+
# noinspection PyTypeHintsInspection,PyTypeHints
8289
class OAuthAuthenticatorBuilder(ABC, Generic[T]):
8390
"""
8491
Abstract builder class for constructing OAuth authenticator instances.

zitadel_client/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def __deepcopy__(self, memo: Dict[int, Any]) -> Self:
8888
memo[id(self)] = result
8989
for k, v in self.__dict__.items():
9090
if k not in ("logger", "logger_file_handler"):
91+
# noinspection PyArgumentList
9192
setattr(result, k, copy.deepcopy(v, memo))
9293
# shallow copy of loggers
9394
result.logger = copy.copy(self.logger)

0 commit comments

Comments
 (0)