Skip to content

Commit da369a7

Browse files
committed
Initial fixes
1 parent cca912d commit da369a7

6 files changed

Lines changed: 74 additions & 52 deletions

File tree

pyproject.toml

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,35 @@ only-packages = true
5050
line-length = 140
5151
target-version = "py39"
5252

53+
5354
[tool.ruff.lint]
54-
select = ["E", "F", "UP", "B", "SIM", "I", "N"]
55-
ignore = ["E501"]
55+
select = [
56+
# pydocstyle
57+
"D",
58+
# flake8-async
59+
"ASYNC",
60+
# pycodestyle
61+
"E",
62+
# Pyflakes
63+
"F",
64+
# pyupgrade
65+
"UP",
66+
# flake8-bugbear
67+
"B",
68+
# flake8-simplify
69+
"SIM",
70+
# isort
71+
"I",
72+
# ruff
73+
"RUF",
74+
# flake8-bandit (security)
75+
"S",
76+
# flake8-print
77+
"T",
78+
# flake8-comprehensions
79+
"C4",
80+
]
81+
ignore = ["E501"] # Line too long
5682

5783
[tool.pytest.ini_options]
5884
asyncio_mode = "auto"
@@ -69,3 +95,15 @@ markers = [
6995
"asyncio: mark test as an asyncio test",
7096
"integration: mark test as an integration test (requires router access)",
7197
]
98+
99+
[tool.mypy]
100+
exclude = ["tests/"]
101+
ignore_missing_imports = true
102+
103+
[tool.ty.src]
104+
exclude = ["tests/"]
105+
106+
[tool.ty.rules]
107+
# This project uses both mypy and ty; mypy's `type: ignore` comments
108+
# should not be flagged as unused by ty.
109+
unused-type-ignore-comment = "ignore"

sagemcom_api/client.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ def __init__(
7979
ssl: bool | None = False,
8080
verify_ssl: bool | None = True,
8181
):
82-
"""
83-
Create a SagemCom client.
82+
"""Create a SagemCom client.
8483
8584
:param host: the host of your Sagemcom router
8685
:param username: the username for your Sagemcom router
@@ -203,7 +202,8 @@ def __get_response_value(self, response, index=0):
203202
value = None
204203

205204
# Rewrite result to snake_case
206-
value = humps.decamelize(value)
205+
if value is not None:
206+
value = humps.decamelize(value)
207207

208208
return value
209209

@@ -231,9 +231,7 @@ async def __post(self, url, data):
231231
error = self.__get_response_error(result)
232232

233233
# No errors
234-
if (
235-
error["description"] == XMO_REQUEST_NO_ERR or error["description"] == "Ok" # NOQA: W503
236-
):
234+
if error["description"] == XMO_REQUEST_NO_ERR or error["description"] == "Ok":
237235
return result
238236

239237
if error["description"] == XMO_INVALID_SESSION_ERR:
@@ -308,7 +306,6 @@ async def __api_request_async(self, actions, priority=False):
308306

309307
async def login(self):
310308
"""Login to the SagemCom F@st router using a username and password."""
311-
312309
actions = {
313310
"id": 0,
314311
"method": "logIn",
@@ -361,7 +358,7 @@ async def logout(self):
361358

362359
async def get_encryption_method(self):
363360
"""Determine which encryption method to use for authentication and set it directly."""
364-
for encryption_method in EncryptionMethod:
361+
for encryption_method in EncryptionMethod: # ty: ignore[not-iterable]
365362
try:
366363
self.authentication_method = encryption_method
367364
self._password_hash = self.__generate_hash(self.password, encryption_method)
@@ -394,8 +391,7 @@ async def get_encryption_method(self):
394391
on_backoff=retry_login,
395392
)
396393
async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> dict:
397-
"""
398-
Retrieve raw value from router using XPath.
394+
"""Retrieve raw value from router using XPath.
399395
400396
:param xpath: path expression
401397
:param options: optional options
@@ -424,8 +420,7 @@ async def get_value_by_xpath(self, xpath: str, options: dict | None = None) -> d
424420
on_backoff=retry_login,
425421
)
426422
async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dict:
427-
"""
428-
Retrieve raw values from router using XPath.
423+
"""Retrieve raw values from router using XPath.
429424
430425
:param xpaths: Dict of key to xpath expression
431426
:param options: optional options
@@ -458,8 +453,7 @@ async def get_values_by_xpaths(self, xpaths, options: dict | None = None) -> dic
458453
on_backoff=retry_login,
459454
)
460455
async def set_value_by_xpath(self, xpath: str, value: str, options: dict | None = None) -> dict:
461-
"""
462-
Retrieve raw value from router using XPath.
456+
"""Retrieve raw value from router using XPath.
463457
464458
:param xpath: path expression
465459
:param value: value

sagemcom_api/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if sys.version_info >= (3, 11):
99
from enum import StrEnum
1010
else:
11-
from backports.strenum import StrEnum
11+
from backports.strenum import StrEnum # ty: ignore[unresolved-import]
1212

1313

1414
@unique

sagemcom_api/models.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import dataclasses
44
from dataclasses import dataclass
5-
from typing import Any, List, Optional
5+
from typing import Any, Optional
66

77

88
# pylint: disable=too-many-instance-attributes
@@ -40,12 +40,12 @@ class Device:
4040
unblock_hours_count: Optional[int] = None
4141
blacklist_status: Optional[bool] = None
4242
blacklisted_according_to_schedule: Optional[bool] = None
43-
blacklisted_schedule: Optional[List] = None
43+
blacklisted_schedule: Optional[list] = None
4444
hidden: Optional[bool] = None
45-
options: Optional[List] = None
45+
options: Optional[list] = None
4646
vendor_class_idv6: Optional[Any] = None
47-
ipv4_addresses: Optional[List] = None
48-
ipv6_addresses: Optional[List] = None
47+
ipv4_addresses: Optional[list] = None
48+
ipv6_addresses: Optional[list] = None
4949
device_type_association: Optional[Any] = None
5050

5151
# pylint:disable=fixme
@@ -60,7 +60,7 @@ def __init__(self, **kwargs):
6060
@property
6161
def id(self):
6262
"""Return unique ID for device."""
63-
return self.phys_address.upper()
63+
return self.phys_address.upper() if self.phys_address else None
6464

6565
@property
6666
def name(self):

tests/conftest.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import json
66
from pathlib import Path
7-
from typing import Any, Dict, List
7+
from typing import Any
88
from unittest.mock import AsyncMock, MagicMock
99

1010
import pytest
@@ -17,9 +17,8 @@
1717
FIXTURES_DIR = Path(__file__).parent / "fixtures"
1818

1919

20-
def load_fixture(filename: str) -> Dict[str, Any]:
21-
"""
22-
Load a JSON fixture file.
20+
def load_fixture(filename: str) -> dict[str, Any]:
21+
"""Load a JSON fixture file.
2322
2423
:param filename: Name of the fixture file (e.g., 'login_success.json')
2524
:return: Parsed JSON data as dict
@@ -30,45 +29,44 @@ def load_fixture(filename: str) -> Dict[str, Any]:
3029

3130

3231
@pytest.fixture
33-
def login_success_response() -> Dict[str, Any]:
32+
def login_success_response() -> dict[str, Any]:
3433
"""Mock response for successful login."""
3534
return load_fixture("login_success.json")
3635

3736

3837
@pytest.fixture
39-
def login_auth_error_response() -> Dict[str, Any]:
38+
def login_auth_error_response() -> dict[str, Any]:
4039
"""Mock response for authentication error."""
4140
return load_fixture("login_auth_error.json")
4241

4342

4443
@pytest.fixture
45-
def login_invalid_session_response() -> Dict[str, Any]:
44+
def login_invalid_session_response() -> dict[str, Any]:
4645
"""Mock response for invalid session error."""
4746
return load_fixture("login_invalid_session.json")
4847

4948

5049
@pytest.fixture
51-
def device_info_response() -> Dict[str, Any]:
50+
def device_info_response() -> dict[str, Any]:
5251
"""Mock response for device info query."""
5352
return load_fixture("device_info.json")
5453

5554

5655
@pytest.fixture
57-
def hosts_response() -> Dict[str, Any]:
56+
def hosts_response() -> dict[str, Any]:
5857
"""Mock response for hosts query."""
5958
return load_fixture("hosts.json")
6059

6160

6261
@pytest.fixture
63-
def xpath_value_response() -> Dict[str, Any]:
62+
def xpath_value_response() -> dict[str, Any]:
6463
"""Mock response for generic XPath getValue."""
6564
return load_fixture("xpath_value.json")
6665

6766

6867
@pytest.fixture
6968
def mock_session_factory():
70-
"""
71-
Factory fixture for creating mock aiohttp ClientSession.
69+
"""Factory fixture for creating mock aiohttp ClientSession.
7270
7371
Returns a factory function that creates a mock session with configurable responses.
7472
Mock responses are consumed in sequence (first call gets first response, etc.).
@@ -80,9 +78,8 @@ def mock_session_factory():
8078
:return: Factory function that takes list of response dicts
8179
"""
8280

83-
def _create_mock_session(responses: List[Dict[str, Any]]) -> ClientSession:
84-
"""
85-
Create a mock ClientSession with specified responses.
81+
def _create_mock_session(responses: list[dict[str, Any]]) -> ClientSession:
82+
"""Create a mock ClientSession with specified responses.
8683
8784
:param responses: List of response dictionaries to return in sequence
8885
:return: Mock ClientSession
@@ -120,8 +117,7 @@ def _create_mock_session(responses: List[Dict[str, Any]]) -> ClientSession:
120117

121118
@pytest.fixture
122119
def mock_client_md5(mock_session_factory, login_success_response):
123-
"""
124-
Create a SagemcomClient with MD5 encryption and mocked session.
120+
"""Create a SagemcomClient with MD5 encryption and mocked session.
125121
126122
Pre-configured with successful login response.
127123
@@ -139,8 +135,7 @@ def mock_client_md5(mock_session_factory, login_success_response):
139135

140136
@pytest.fixture
141137
def mock_client_sha512(mock_session_factory, login_success_response):
142-
"""
143-
Create a SagemcomClient with SHA512 encryption and mocked session.
138+
"""Create a SagemcomClient with SHA512 encryption and mocked session.
144139
145140
Pre-configured with successful login response.
146141
@@ -158,8 +153,7 @@ def mock_client_sha512(mock_session_factory, login_success_response):
158153

159154
@pytest.fixture
160155
def mock_client_md5_nonce(mock_session_factory, login_success_response):
161-
"""
162-
Create a SagemcomClient with MD5_NONCE encryption and mocked session.
156+
"""Create a SagemcomClient with MD5_NONCE encryption and mocked session.
163157
164158
Pre-configured with successful login response.
165159

tests/unit/test_client_basic.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111

1212
@pytest.mark.asyncio
1313
async def test_login_success(mock_session_factory, login_success_response):
14-
"""
15-
Test successful login with mocked session.
14+
"""Test successful login with mocked session.
1615
1716
Demonstrates:
1817
- Mocking aiohttp session at session.post level
@@ -41,8 +40,7 @@ async def test_login_success(mock_session_factory, login_success_response):
4140

4241
@pytest.mark.asyncio
4342
async def test_login_authentication_error(mock_session_factory, login_auth_error_response):
44-
"""
45-
Test login raises AuthenticationException on XMO_AUTHENTICATION_ERR.
43+
"""Test login raises AuthenticationException on XMO_AUTHENTICATION_ERR.
4644
4745
Demonstrates:
4846
- Mocking error responses from the API
@@ -69,8 +67,7 @@ async def test_login_authentication_error(mock_session_factory, login_auth_error
6967

7068
@pytest.mark.asyncio
7169
async def test_get_value_by_xpath_url_encoding(mock_session_factory, login_success_response, xpath_value_response):
72-
"""
73-
Test XPath values are URL-encoded with safe characters preserved.
70+
"""Test XPath values are URL-encoded with safe characters preserved.
7471
7572
Demonstrates:
7673
- XPath URL encoding behavior
@@ -108,8 +105,7 @@ async def test_get_value_by_xpath_url_encoding(mock_session_factory, login_succe
108105

109106
@pytest.mark.asyncio
110107
async def test_login_with_preconfigured_fixture(mock_client_sha512):
111-
"""
112-
Test login using pre-configured client fixture.
108+
"""Test login using pre-configured client fixture.
113109
114110
Demonstrates:
115111
- Using pre-configured client fixtures for concise tests

0 commit comments

Comments
 (0)