controlid-sdk 0.3.3__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlid-sdk
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: A production-ready asynchronous Python SDK for ControlID access control devices, supporting biometrics, networking, and complex time schedules.
5
5
  Author-email: Tulio Amancio <root@tsuriu.com.br>
6
6
  License: MIT
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
- Classifier: License :: OSI Approved :: MIT License
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
17
  Classifier: Topic :: System :: Hardware
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "controlid-sdk"
7
- version = "0.3.3"
7
+ version = "0.3.4"
8
8
  authors = [
9
9
  { name="Tulio Amancio", email="root@tsuriu.com.br" },
10
10
  ]
11
11
  description = "A production-ready asynchronous Python SDK for ControlID access control devices, supporting biometrics, networking, and complex time schedules."
12
12
  readme = "README.md"
13
- license = { text = "MIT" }
13
+ license = {text = "MIT"}
14
14
  requires-python = ">=3.9"
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
@@ -20,7 +20,6 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.10",
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
- "License :: OSI Approved :: MIT License",
24
23
  "Operating System :: OS Independent",
25
24
  "Topic :: Software Development :: Libraries :: Python Modules",
26
25
  "Topic :: System :: Hardware",
@@ -11,7 +11,6 @@ from .models import (
11
11
  Door,
12
12
  AccessLog,
13
13
  Device,
14
- TimeZone,
15
14
  TimeSpan,
16
15
  FaceTemplate,
17
16
  CatraInfo,
@@ -32,7 +31,7 @@ from .exceptions import (
32
31
  )
33
32
  from . import constants
34
33
 
35
- __version__ = "0.3.3"
34
+ __version__ = "0.3.4"
36
35
 
37
36
  __all__ = [
38
37
  "ControlIDClient",
@@ -44,6 +43,7 @@ __all__ = [
44
43
  "UserGroup",
45
44
  "AccessRule",
46
45
  "TimeZone",
46
+ "TimeSpan",
47
47
  "Area",
48
48
  "Door",
49
49
  "AccessLog",
@@ -67,3 +67,4 @@ __all__ = [
67
67
  "constants",
68
68
  ]
69
69
 
70
+
@@ -2,10 +2,11 @@ import httpx
2
2
  import asyncio
3
3
  import time as _time
4
4
  from typing import List, Dict, Any, Optional, Union
5
- from urllib.parse import quote, urlparse
5
+ from urllib.parse import quote
6
6
  from . import constants as const
7
7
  from . import exceptions as ex
8
- from .models import User, Card, Door, AccessLog, UserRole, QRCard, CustomField, TimeZone, TimeSpan
8
+ from .models import User, Card, AccessLog, UserRole, CustomField, TimeZone, TimeSpan
9
+
9
10
 
10
11
 
11
12
  def _normalize_host(host: str) -> str:
@@ -107,14 +108,12 @@ class ControlIDClient:
107
108
  payload = {"login": self.user, "password": self.password}
108
109
 
109
110
  try:
110
- logger_ctx = {"host": try_host, "user": self.user}
111
111
  response = await self._client.post(url, json=payload, timeout=5.0) # Short timeout for login attempt
112
112
  response.raise_for_status()
113
113
  data = response.json()
114
114
  if "session" in data:
115
115
  self.session = data["session"]
116
116
  if self.host != try_host:
117
- old_host = self.host
118
117
  self.host = try_host # Update to working host
119
118
  if self.on_host_change:
120
119
  try:
@@ -122,14 +121,16 @@ class ControlIDClient:
122
121
  asyncio.create_task(self.on_host_change(self.host))
123
122
  else:
124
123
  self.on_host_change(self.host)
125
- except Exception: pass
124
+ except Exception:
125
+ pass
126
126
  return self.session
127
127
  raise ex.AuthenticationError(f"Login failed: {data}")
128
128
  except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout, httpx.WriteTimeout, httpx.PoolTimeout, httpx.ProtocolError, httpx.RemoteProtocolError, httpx.ProxyError) as e:
129
129
  last_err = f"{type(e).__name__} on {proto}"
130
130
  continue
131
131
  except Exception as e:
132
- if isinstance(e, ex.AuthenticationError): raise
132
+ if isinstance(e, ex.AuthenticationError):
133
+ raise
133
134
  last_err = f"{type(e).__name__}: {str(e)}"
134
135
  continue
135
136
 
@@ -228,7 +229,8 @@ class ControlIDClient:
228
229
  except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout, httpx.WriteTimeout, httpx.PoolTimeout) as e:
229
230
  raise ex.DeviceUnreachableError(f"Device unreachable: {type(e).__name__} {e}")
230
231
  except Exception as e:
231
- if isinstance(e, (ex.APIError, ex.AuthenticationError, ex.DeviceUnreachableError)): raise
232
+ if isinstance(e, (ex.APIError, ex.AuthenticationError, ex.DeviceUnreachableError)):
233
+ raise
232
234
  raise ex.ControlIDError(f"Request failed: {type(e).__name__} {e}")
233
235
 
234
236
  async def get_system_information(self) -> Dict[str, Any]:
@@ -370,8 +372,10 @@ class ControlIDClient:
370
372
  async def get_cards(self, user_id: Optional[int] = None, value: Optional[int] = None) -> List[Card]:
371
373
  """Return credentials for all users, or filtered by user_id or card value."""
372
374
  where = {}
373
- if user_id: where["user_id"] = user_id
374
- if value: where["value"] = value
375
+ if user_id:
376
+ where["user_id"] = user_id
377
+ if value:
378
+ where["value"] = value
375
379
  return [Card(**c) for c in await self.load_objects(const.TABLE_CARDS, where=where or None)]
376
380
 
377
381
  async def add_card(self, card: Card) -> int:
@@ -767,6 +771,8 @@ class ControlIDClient:
767
771
 
768
772
  await client.set_configuration({"face_id": {"qrcode_legacy_mode_enabled": "1"}})
769
773
  """
774
+ return await self.request(const.SET_CONFIGURATION, config)
775
+
770
776
  async def export_audit_logs(self, config: int = 1, api: int = 1, usb: int = 0, network: int = 0, time: int = 1, online: int = 0, menu: int = 1) -> List[Dict[str, Any]]:
771
777
  """
772
778
  Export device audit logs (system events, config changes, etc.).
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
- from typing import Optional, List, Dict, Any
1
+ from pydantic import BaseModel
2
+ from typing import Optional, Dict, Any
3
3
 
4
4
 
5
5
  # ─── Core Models ───────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlid-sdk
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: A production-ready asynchronous Python SDK for ControlID access control devices, supporting biometrics, networking, and complex time schedules.
5
5
  Author-email: Tulio Amancio <root@tsuriu.com.br>
6
6
  License: MIT
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
- Classifier: License :: OSI Approved :: MIT License
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
17
  Classifier: Topic :: System :: Hardware
@@ -96,3 +96,14 @@ async def test_client_request_device_unreachable():
96
96
  with patch.object(client._client, "post", new_callable=AsyncMock, side_effect=httpx.ConnectError("Connection refused")):
97
97
  with pytest.raises(DeviceUnreachableError):
98
98
  await client.request("/some-endpoint")
99
+
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_set_configuration_calls_hardware_endpoint():
103
+ client = ControlIDClient(host="https://192.168.0.100")
104
+ payload = {"snmp_agent": {"snmp_enabled": "1"}}
105
+ with patch.object(client, "request", new_callable=AsyncMock, return_value={"status": "ok"}) as request:
106
+ result = await client.set_configuration(payload)
107
+
108
+ request.assert_awaited_once_with("/set_configuration.fcgi", payload)
109
+ assert result == {"status": "ok"}
File without changes
File without changes