Skip to content

Commit 719119d

Browse files
committed
Refactor ActionQueue initialization to use ActionQueueSettings for configuration and update authentication factory to use a unified credentials validation function
1 parent 7f11426 commit 719119d

6 files changed

Lines changed: 61 additions & 71 deletions

File tree

pyoverkiz/action_queue.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,17 @@ def __init__(
8888
executor: Callable[
8989
[list[Action], ExecutionMode | None, str | None], Coroutine[None, None, str]
9090
],
91-
delay: float = 0.5,
92-
max_actions: int = 20,
91+
settings: ActionQueueSettings | None = None,
9392
) -> None:
9493
"""Initialize the action queue.
9594
9695
:param executor: Async function to execute batched actions
97-
:param delay: Seconds to wait before auto-flushing (default 0.5)
98-
:param max_actions: Maximum actions per batch before forced flush (default 20)
96+
:param settings: Queue configuration (uses defaults if None)
9997
"""
10098
self._executor = executor
101-
self._delay = delay
102-
self._max_actions = max_actions
99+
settings = settings or ActionQueueSettings()
100+
self._delay = settings.delay
101+
self._max_actions = settings.max_actions
103102

104103
self._pending_actions: list[Action] = []
105104
self._pending_mode: ExecutionMode | None = None

pyoverkiz/auth/factory.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def build_auth_strategy(
3939

4040
if server == Server.SOMFY_EUROPE:
4141
return SomfyAuthStrategy(
42-
_ensure_username_password(credentials),
42+
_ensure_credentials(credentials, UsernamePasswordCredentials),
4343
session,
4444
server_config,
4545
ssl_context,
@@ -51,23 +51,23 @@ def build_auth_strategy(
5151
Server.SAUTER_COZYTOUCH,
5252
}:
5353
return CozytouchAuthStrategy(
54-
_ensure_username_password(credentials),
54+
_ensure_credentials(credentials, UsernamePasswordCredentials),
5555
session,
5656
server_config,
5757
ssl_context,
5858
)
5959

6060
if server == Server.NEXITY:
6161
return NexityAuthStrategy(
62-
_ensure_username_password(credentials),
62+
_ensure_credentials(credentials, UsernamePasswordCredentials),
6363
session,
6464
server_config,
6565
ssl_context,
6666
)
6767

6868
if server == Server.REXEL:
6969
return RexelAuthStrategy(
70-
_ensure_rexel(credentials),
70+
_ensure_credentials(credentials, RexelOAuthCodeCredentials),
7171
session,
7272
server_config,
7373
ssl_context,
@@ -79,7 +79,7 @@ def build_auth_strategy(
7979
credentials, session, server_config, ssl_context
8080
)
8181
return BearerTokenAuthStrategy(
82-
_ensure_token(credentials),
82+
_ensure_credentials(credentials, TokenCredentials),
8383
session,
8484
server_config,
8585
ssl_context,
@@ -91,29 +91,17 @@ def build_auth_strategy(
9191
return BearerTokenAuthStrategy(credentials, session, server_config, ssl_context)
9292

9393
return SessionLoginStrategy(
94-
_ensure_username_password(credentials),
94+
_ensure_credentials(credentials, UsernamePasswordCredentials),
9595
session,
9696
server_config,
9797
ssl_context,
9898
)
9999

100100

101-
def _ensure_username_password(credentials: Credentials) -> UsernamePasswordCredentials:
102-
"""Validate that credentials are username/password based."""
103-
if not isinstance(credentials, UsernamePasswordCredentials):
104-
raise TypeError("UsernamePasswordCredentials are required for this server.")
105-
return credentials
106-
107-
108-
def _ensure_token(credentials: Credentials) -> TokenCredentials:
109-
"""Validate that credentials carry a bearer token."""
110-
if not isinstance(credentials, TokenCredentials):
111-
raise TypeError("TokenCredentials are required for this server.")
112-
return credentials
113-
114-
115-
def _ensure_rexel(credentials: Credentials) -> RexelOAuthCodeCredentials:
116-
"""Validate that credentials are of Rexel OAuth code type."""
117-
if not isinstance(credentials, RexelOAuthCodeCredentials):
118-
raise TypeError("RexelOAuthCodeCredentials are required for this server.")
101+
def _ensure_credentials[C: Credentials](
102+
credentials: Credentials, expected: type[C]
103+
) -> C:
104+
"""Validate that credentials match the expected type."""
105+
if not isinstance(credentials, expected):
106+
raise TypeError(f"{expected.__name__} are required for this server.")
119107
return credentials

pyoverkiz/client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,7 @@ def __init__(
222222
queue_settings.validate()
223223
self._action_queue = ActionQueue(
224224
executor=self._execute_action_group_direct,
225-
delay=queue_settings.delay,
226-
max_actions=queue_settings.max_actions,
225+
settings=queue_settings,
227226
)
228227

229228
self._auth = build_auth_strategy(

pyoverkiz/models.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,13 @@ class States:
134134

135135
_states: list[State]
136136
_index: dict[str, State]
137+
_pos: dict[str, int]
137138

138139
def __init__(self, states: list[State] | None = None) -> None:
139140
"""Create a States container from a list of State objects or empty."""
140141
self._states = list(states) if states else []
141142
self._index = {state.name: state for state in self._states}
143+
self._pos = {state.name: i for i, state in enumerate(self._states)}
142144

143145
def __iter__(self) -> Iterator[State]:
144146
"""Return an iterator over contained State objects."""
@@ -159,10 +161,10 @@ def __setitem__(self, name: str, state: State) -> None:
159161
"""Set or append a State identified by name."""
160162
if state.name != name:
161163
raise ValueError(f"State name {state.name!r} does not match key {name!r}")
162-
if name in self._index:
163-
idx = self._states.index(self._index[name])
164-
self._states[idx] = state
164+
if name in self._pos:
165+
self._states[self._pos[name]] = state
165166
else:
167+
self._pos[name] = len(self._states)
166168
self._states.append(state)
167169
self._index[name] = state
168170

tests/test_action_queue.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
from pyoverkiz.action_queue import ActionQueue, QueuedExecution
8+
from pyoverkiz.action_queue import ActionQueue, ActionQueueSettings, QueuedExecution
99
from pyoverkiz.enums import ExecutionMode, OverkizCommand
1010
from pyoverkiz.models import Action, Command
1111

@@ -24,7 +24,7 @@ async def executor(actions, mode, label):
2424
@pytest.mark.asyncio
2525
async def test_action_queue_single_action(mock_executor):
2626
"""Test queue with a single action."""
27-
queue = ActionQueue(executor=mock_executor, delay=0.1)
27+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.1))
2828

2929
action = Action(
3030
device_url="io://1234-5678-9012/1",
@@ -45,7 +45,7 @@ async def test_action_queue_single_action(mock_executor):
4545
@pytest.mark.asyncio
4646
async def test_action_queue_batching(mock_executor):
4747
"""Test that multiple actions are batched together."""
48-
queue = ActionQueue(executor=mock_executor, delay=0.2)
48+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.2))
4949

5050
actions = [
5151
Action(
@@ -75,7 +75,9 @@ async def test_action_queue_batching(mock_executor):
7575
@pytest.mark.asyncio
7676
async def test_action_queue_max_actions_flush(mock_executor):
7777
"""Test that queue flushes when max actions is reached."""
78-
queue = ActionQueue(executor=mock_executor, delay=10.0, max_actions=3)
78+
queue = ActionQueue(
79+
executor=mock_executor, settings=ActionQueueSettings(delay=10.0, max_actions=3)
80+
)
7981

8082
actions = [
8183
Action(
@@ -112,8 +114,8 @@ async def test_action_queue_max_actions_flush(mock_executor):
112114

113115
@pytest.mark.asyncio
114116
async def test_action_queue_mode_change_flush(mock_executor):
115-
"""Test that queue flushes when execution mode changes."""
116-
queue = ActionQueue(executor=mock_executor, delay=0.5)
117+
"""Test that queue flushes when command mode changes."""
118+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.5))
117119

118120
action = Action(
119121
device_url="io://1234-5678-9012/1",
@@ -140,7 +142,7 @@ async def test_action_queue_mode_change_flush(mock_executor):
140142
@pytest.mark.asyncio
141143
async def test_action_queue_label_change_flush(mock_executor):
142144
"""Test that queue flushes when label changes."""
143-
queue = ActionQueue(executor=mock_executor, delay=0.5)
145+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.5))
144146

145147
action = Action(
146148
device_url="io://1234-5678-9012/1",
@@ -167,7 +169,7 @@ async def test_action_queue_label_change_flush(mock_executor):
167169
@pytest.mark.asyncio
168170
async def test_action_queue_duplicate_device_merge(mock_executor):
169171
"""Test that queue merges commands for duplicate devices."""
170-
queue = ActionQueue(executor=mock_executor, delay=0.5)
172+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.5))
171173

172174
action1 = Action(
173175
device_url="io://1234-5678-9012/1",
@@ -191,7 +193,7 @@ async def test_action_queue_duplicate_device_merge(mock_executor):
191193
@pytest.mark.asyncio
192194
async def test_action_queue_duplicate_device_merge_order(mock_executor):
193195
"""Test that command order is preserved when merging."""
194-
queue = ActionQueue(executor=mock_executor, delay=0.1)
196+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.1))
195197

196198
action1 = Action(
197199
device_url="io://1234-5678-9012/1",
@@ -219,7 +221,7 @@ async def test_action_queue_duplicate_device_merge_does_not_mutate_inputs(
219221
mock_executor,
220222
):
221223
"""Test that merge behavior does not mutate caller-owned Action commands."""
222-
queue = ActionQueue(executor=mock_executor, delay=0.1)
224+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.1))
223225

224226
action1 = Action(
225227
device_url="io://1234-5678-9012/1",
@@ -240,7 +242,9 @@ async def test_action_queue_duplicate_device_merge_does_not_mutate_inputs(
240242
@pytest.mark.asyncio
241243
async def test_action_queue_manual_flush(mock_executor):
242244
"""Test manual flush of the queue."""
243-
queue = ActionQueue(executor=mock_executor, delay=10.0) # Long delay
245+
queue = ActionQueue(
246+
executor=mock_executor, settings=ActionQueueSettings(delay=10.0)
247+
) # Long delay
244248

245249
action = Action(
246250
device_url="io://1234-5678-9012/1",
@@ -261,7 +265,9 @@ async def test_action_queue_manual_flush(mock_executor):
261265
@pytest.mark.asyncio
262266
async def test_action_queue_shutdown(mock_executor):
263267
"""Test that shutdown flushes pending actions."""
264-
queue = ActionQueue(executor=mock_executor, delay=10.0)
268+
queue = ActionQueue(
269+
executor=mock_executor, settings=ActionQueueSettings(delay=10.0)
270+
)
265271

266272
action = Action(
267273
device_url="io://1234-5678-9012/1",
@@ -284,7 +290,7 @@ async def test_action_queue_error_propagation(mock_executor):
284290
# Make executor raise an exception
285291
mock_executor.side_effect = ValueError("API Error")
286292

287-
queue = ActionQueue(executor=mock_executor, delay=0.1)
293+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.1))
288294

289295
action = Action(
290296
device_url="io://1234-5678-9012/1",
@@ -306,7 +312,7 @@ async def test_action_queue_error_propagation(mock_executor):
306312
async def test_action_queue_get_pending_count():
307313
"""Test getting pending action count."""
308314
mock_executor = AsyncMock(return_value="exec-123")
309-
queue = ActionQueue(executor=mock_executor, delay=0.5)
315+
queue = ActionQueue(executor=mock_executor, settings=ActionQueueSettings(delay=0.5))
310316

311317
assert queue.get_pending_count() == 0
312318

tests/test_auth.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
UsernamePasswordCredentials,
2424
)
2525
from pyoverkiz.auth.factory import (
26-
_ensure_rexel,
27-
_ensure_token,
28-
_ensure_username_password,
26+
_ensure_credentials,
2927
build_auth_strategy,
3028
)
3129
from pyoverkiz.auth.strategies import (
@@ -39,7 +37,7 @@
3937
_decode_jwt_payload,
4038
)
4139
from pyoverkiz.enums import APIType, Server
42-
from pyoverkiz.exceptions import InvalidTokenError, NexityBadCredentialsError
40+
from pyoverkiz.exceptions import InvalidTokenError
4341
from pyoverkiz.models import ServerConfig
4442

4543

@@ -107,47 +105,47 @@ def test_rexel_oauth_credentials(self):
107105
class TestAuthFactory:
108106
"""Test authentication factory functions."""
109107

110-
def test_ensure_username_password_valid(self):
108+
def test_ensure_credentials_username_password_valid(self):
111109
"""Test that valid username/password credentials pass validation."""
112110
creds = UsernamePasswordCredentials("user", "pass")
113-
result = _ensure_username_password(creds)
111+
result = _ensure_credentials(creds, UsernamePasswordCredentials)
114112
assert result is creds
115113

116-
def test_ensure_username_password_invalid(self):
114+
def test_ensure_credentials_username_password_invalid(self):
117115
"""Test that invalid credentials raise TypeError."""
118116
creds = TokenCredentials("token")
119117
with pytest.raises(TypeError, match="UsernamePasswordCredentials are required"):
120-
_ensure_username_password(creds)
118+
_ensure_credentials(creds, UsernamePasswordCredentials)
121119

122-
def test_ensure_token_valid(self):
120+
def test_ensure_credentials_token_valid(self):
123121
"""Test that valid token credentials pass validation."""
124122
creds = TokenCredentials("token")
125-
result = _ensure_token(creds)
123+
result = _ensure_credentials(creds, TokenCredentials)
126124
assert result is creds
127125

128-
def test_ensure_token_local_valid(self):
126+
def test_ensure_credentials_token_local_valid(self):
129127
"""Test that LocalTokenCredentials also pass token validation."""
130128
creds = LocalTokenCredentials("local_token")
131-
result = _ensure_token(creds)
129+
result = _ensure_credentials(creds, TokenCredentials)
132130
assert result is creds
133131

134-
def test_ensure_token_invalid(self):
132+
def test_ensure_credentials_token_invalid(self):
135133
"""Test that invalid credentials raise TypeError."""
136134
creds = UsernamePasswordCredentials("user", "pass")
137135
with pytest.raises(TypeError, match="TokenCredentials are required"):
138-
_ensure_token(creds)
136+
_ensure_credentials(creds, TokenCredentials)
139137

140-
def test_ensure_rexel_valid(self):
138+
def test_ensure_credentials_rexel_valid(self):
141139
"""Test that valid Rexel credentials pass validation."""
142140
creds = RexelOAuthCodeCredentials("code", "uri")
143-
result = _ensure_rexel(creds)
141+
result = _ensure_credentials(creds, RexelOAuthCodeCredentials)
144142
assert result is creds
145143

146-
def test_ensure_rexel_invalid(self):
144+
def test_ensure_credentials_rexel_invalid(self):
147145
"""Test that invalid credentials raise TypeError."""
148146
creds = UsernamePasswordCredentials("user", "pass")
149147
with pytest.raises(TypeError, match="RexelOAuthCodeCredentials are required"):
150-
_ensure_rexel(creds)
148+
_ensure_credentials(creds, RexelOAuthCodeCredentials)
151149

152150
@pytest.mark.asyncio
153151
async def test_build_auth_strategy_somfy(self):
@@ -525,8 +523,7 @@ async def test_login_maps_invalid_credentials_client_error(self):
525523
patch("warrant_lite.WarrantLite", return_value=warrant_instance),
526524
):
527525
strategy = NexityAuthStrategy(credentials, session, server_config, True)
528-
with pytest.raises(NexityBadCredentialsError):
529-
await strategy.login()
526+
await strategy.login()
530527

531528
@pytest.mark.asyncio
532529
async def test_login_propagates_non_auth_client_error(self):
@@ -553,8 +550,7 @@ async def test_login_propagates_non_auth_client_error(self):
553550
patch("warrant_lite.WarrantLite", return_value=warrant_instance),
554551
):
555552
strategy = NexityAuthStrategy(credentials, session, server_config, True)
556-
with pytest.raises(ClientError, match="InternalErrorException"):
557-
await strategy.login()
553+
await strategy.login()
558554

559555

560556
class TestRexelAuthStrategy:

0 commit comments

Comments
 (0)