place-integration-api 0.2.4__tar.gz → 0.2.6__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.
Files changed (33) hide show
  1. {place_integration_api-0.2.4/src/place_integration_api.egg-info → place_integration_api-0.2.6}/PKG-INFO +16 -2
  2. place_integration_api-0.2.6/README.md +15 -0
  3. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/pyproject.toml +4 -2
  4. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/auth/abstract_auth.py +3 -1
  5. place_integration_api-0.2.6/src/place/errors.py +9 -0
  6. place_integration_api-0.2.6/src/place/provider.py +43 -0
  7. {place_integration_api-0.2.4 → place_integration_api-0.2.6/src/place_integration_api.egg-info}/PKG-INFO +16 -2
  8. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place_integration_api.egg-info/SOURCES.txt +1 -0
  9. place_integration_api-0.2.6/tests/test_auth.py +70 -0
  10. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/tests/test_provider.py +79 -0
  11. place_integration_api-0.2.4/README.md +0 -3
  12. place_integration_api-0.2.4/src/place/provider.py +0 -38
  13. place_integration_api-0.2.4/tests/test_auth.py +0 -31
  14. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/LICENSE +0 -0
  15. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/setup.cfg +0 -0
  16. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/auth/__init__.py +0 -0
  17. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/auth/aws_srp.py +0 -0
  18. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/auth/srp_auth.py +0 -0
  19. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/auth/tokens.py +0 -0
  20. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/config.py +0 -0
  21. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/messages.py +0 -0
  22. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/__init__.py +0 -0
  23. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/credentials.py +0 -0
  24. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/device_shadow.py +0 -0
  25. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/discover_device.py +0 -0
  26. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/models.py +0 -0
  27. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/models/mqtt_message.py +0 -0
  28. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place/mqtt_client.py +0 -0
  29. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place_integration_api.egg-info/dependency_links.txt +0 -0
  30. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place_integration_api.egg-info/requires.txt +0 -0
  31. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/src/place_integration_api.egg-info/top_level.txt +0 -0
  32. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/tests/test_models.py +0 -0
  33. {place_integration_api-0.2.4 → place_integration_api-0.2.6}/tests/test_mqtt_client.py +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: place-integration-api
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place.
5
5
  Author-email: Nicholas Aelick <niaexa@syntronic.com>, Andrew Tran <andrew.tran@gentex.com>
6
- Project-URL: Homepage, https://pypi.org/project/place-integration-api/
6
+ Project-URL: Homepage, https://placehomesolutions.com
7
+ Project-URL: Issues, https://placehomesolutions.com/support
8
+ Project-URL: Repository, https://github.com/Gentex-Corporation/place-integration-api
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: License :: OSI Approved :: MIT License
9
11
  Classifier: Operating System :: OS Independent
@@ -25,3 +27,15 @@ Dynamic: license-file
25
27
  # Place integration API
26
28
 
27
29
  This project contains a python API to connect MQTT data from Place devices to Place's Home Assistant integration
30
+
31
+ ## Setup
32
+
33
+ This repo utilizes VS Code [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) so you don't need to alter your global python environment.
34
+
35
+ ## Installing dependencies
36
+
37
+ Use `pip` to install required dependencies by running `pip install -r requirements.txt` from the project root.
38
+
39
+ ## Updating dependencies
40
+
41
+ During the course of development, if new packages are added, it's recommended to update [`requirements.txt`](./requirements.txt) by running `pip freeze > requirements.txt`.
@@ -0,0 +1,15 @@
1
+ # Place integration API
2
+
3
+ This project contains a python API to connect MQTT data from Place devices to Place's Home Assistant integration
4
+
5
+ ## Setup
6
+
7
+ This repo utilizes VS Code [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) so you don't need to alter your global python environment.
8
+
9
+ ## Installing dependencies
10
+
11
+ Use `pip` to install required dependencies by running `pip install -r requirements.txt` from the project root.
12
+
13
+ ## Updating dependencies
14
+
15
+ During the course of development, if new packages are added, it's recommended to update [`requirements.txt`](./requirements.txt) by running `pip freeze > requirements.txt`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "place-integration-api"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -34,7 +34,9 @@ dev = [
34
34
  ]
35
35
 
36
36
  [project.urls]
37
- Homepage = "https://pypi.org/project/place-integration-api/"
37
+ Homepage = "https://placehomesolutions.com"
38
+ Issues = "https://placehomesolutions.com/support"
39
+ Repository = "https://github.com/Gentex-Corporation/place-integration-api"
38
40
 
39
41
  [tool.pytest.ini_options]
40
42
  testpaths = ["tests"]
@@ -25,9 +25,11 @@ class AbstractAuth(ABC):
25
25
  access_token = await self.async_get_access_token()
26
26
  headers["authorization"] = f"Bearer {access_token}"
27
27
 
28
- return await self.websession.request(
28
+ resp = await self.websession.request(
29
29
  method,
30
30
  url,
31
31
  **kwargs,
32
32
  headers=headers,
33
33
  )
34
+ resp.raise_for_status()
35
+ return resp
@@ -0,0 +1,9 @@
1
+ """Errors for the Place API."""
2
+
3
+
4
+ class PlaceApiError(Exception):
5
+ """Base error for Place API failures."""
6
+
7
+
8
+ class PlaceFulfillmentError(PlaceApiError):
9
+ """Raised when the fulfillment API reports a business-logic failure."""
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from aiohttp import ContentTypeError
6
+
7
+ from .auth.abstract_auth import AbstractAuth
8
+ from .errors import PlaceFulfillmentError
9
+ from .models.discover_device import DiscoverDevice
10
+ from .config import FULFILLMENT_URL
11
+
12
+
13
+ async def _send_command(authorized_session: AbstractAuth, command: str) -> dict[str, Any]:
14
+ """Send a fulfillment command and return the parsed response body."""
15
+ body = {"command": command, "data": {}}
16
+ resp = await authorized_session.request("POST", FULFILLMENT_URL, json=body)
17
+ try:
18
+ data = await resp.json()
19
+ except (ContentTypeError, ValueError) as err:
20
+ raise PlaceFulfillmentError(
21
+ f"Invalid response from fulfillment API: {err}"
22
+ ) from err
23
+ if not isinstance(data, dict):
24
+ raise PlaceFulfillmentError(f"Invalid response from fulfillment API: {data!r}")
25
+ if not data.get("success", True):
26
+ raise PlaceFulfillmentError(f"Home Assistant error: {data.get('message', data)}")
27
+ return data
28
+
29
+
30
+ class Provider:
31
+ def __init__(self, authorized_session: AbstractAuth) -> None:
32
+ self.authorized_session = authorized_session
33
+
34
+ async def discover(self) -> list[DiscoverDevice]:
35
+ data = await _send_command(self.authorized_session, "DISCOVER")
36
+ devices_raw = (data.get("data") or {}).get("devices") or []
37
+ return [DiscoverDevice.from_dict(raw) for raw in devices_raw]
38
+
39
+ async def enable(self):
40
+ return await _send_command(self.authorized_session, "ENABLE")
41
+
42
+ async def disable(self):
43
+ return await _send_command(self.authorized_session, "DISABLE")
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: place-integration-api
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python APIs for Cognito-authenticated MQTT flows and Home Assistant discovery for Gentex Place.
5
5
  Author-email: Nicholas Aelick <niaexa@syntronic.com>, Andrew Tran <andrew.tran@gentex.com>
6
- Project-URL: Homepage, https://pypi.org/project/place-integration-api/
6
+ Project-URL: Homepage, https://placehomesolutions.com
7
+ Project-URL: Issues, https://placehomesolutions.com/support
8
+ Project-URL: Repository, https://github.com/Gentex-Corporation/place-integration-api
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: License :: OSI Approved :: MIT License
9
11
  Classifier: Operating System :: OS Independent
@@ -25,3 +27,15 @@ Dynamic: license-file
25
27
  # Place integration API
26
28
 
27
29
  This project contains a python API to connect MQTT data from Place devices to Place's Home Assistant integration
30
+
31
+ ## Setup
32
+
33
+ This repo utilizes VS Code [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) so you don't need to alter your global python environment.
34
+
35
+ ## Installing dependencies
36
+
37
+ Use `pip` to install required dependencies by running `pip install -r requirements.txt` from the project root.
38
+
39
+ ## Updating dependencies
40
+
41
+ During the course of development, if new packages are added, it's recommended to update [`requirements.txt`](./requirements.txt) by running `pip freeze > requirements.txt`.
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/place/config.py
5
+ src/place/errors.py
5
6
  src/place/messages.py
6
7
  src/place/mqtt_client.py
7
8
  src/place/provider.py
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from unittest.mock import AsyncMock, MagicMock, patch
5
+
6
+ import pytest
7
+ from aiohttp import ClientResponseError
8
+
9
+ from place.auth import get_iot_credentials
10
+ from place.auth.abstract_auth import AbstractAuth
11
+
12
+
13
+ class _FakeAuth(AbstractAuth):
14
+ async def async_get_access_token(self) -> str:
15
+ return "token"
16
+
17
+
18
+ @patch("place.auth.srp_auth.boto3")
19
+ def test_get_iot_credentials_success(mock_boto3: MagicMock) -> None:
20
+ identity_client = MagicMock()
21
+ mock_boto3.client.return_value = identity_client
22
+
23
+ identity_client.get_id.return_value = {"IdentityId": "identity-123"}
24
+ identity_client.get_credentials_for_identity.return_value = {
25
+ "Credentials": {
26
+ "AccessKeyId": "AKIA...",
27
+ "SecretKey": "secret",
28
+ "SessionToken": "session",
29
+ }
30
+ }
31
+
32
+
33
+ creds = get_iot_credentials(
34
+ id_token="id-token",
35
+ access_token="access-token",
36
+ )
37
+
38
+ assert creds.access_key_id == "AKIA..."
39
+ assert creds.secret_access_key == "secret"
40
+ assert creds.session_token == "session"
41
+ assert creds.identity_id == "identity-123"
42
+ assert creds.access_token == "access-token"
43
+
44
+
45
+ def test_request_raises_for_non_2xx_status() -> None:
46
+ response = MagicMock()
47
+ response.raise_for_status.side_effect = ClientResponseError(
48
+ request_info=MagicMock(), history=(), status=401
49
+ )
50
+ websession = MagicMock()
51
+ websession.request = AsyncMock(return_value=response)
52
+
53
+ auth = _FakeAuth(websession)
54
+
55
+ with pytest.raises(ClientResponseError):
56
+ asyncio.run(auth.request("POST", "https://example.com"))
57
+
58
+
59
+ def test_request_returns_response_for_2xx_status() -> None:
60
+ response = MagicMock()
61
+ response.raise_for_status.return_value = None
62
+ websession = MagicMock()
63
+ websession.request = AsyncMock(return_value=response)
64
+
65
+ auth = _FakeAuth(websession)
66
+
67
+ result = asyncio.run(auth.request("POST", "https://example.com"))
68
+
69
+ assert result is response
70
+ response.raise_for_status.assert_called_once()
@@ -2,8 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
 
5
+ import pytest
6
+
5
7
  from place.auth.abstract_auth import AbstractAuth
6
8
  from place.config import FULFILLMENT_URL
9
+ from place.errors import PlaceFulfillmentError
7
10
  from place.provider import Provider
8
11
 
9
12
 
@@ -101,3 +104,79 @@ def test_provider_disable_sends_disable_command() -> None:
101
104
 
102
105
  assert auth.calls == [("POST", FULFILLMENT_URL, {"json": {"command": "DISABLE", "data": {}}})]
103
106
  assert result == payload
107
+
108
+
109
+ def test_provider_discover_raises_on_success_false() -> None:
110
+ payload = {"success": False, "message": "not authorized"}
111
+
112
+ class DummyAuth(AbstractAuth):
113
+ async def async_get_access_token(self) -> str:
114
+ return "token"
115
+
116
+ async def request(self, method, url, **kwargs):
117
+ class DummyResponse:
118
+ async def json(self_inner):
119
+ return payload
120
+
121
+ return DummyResponse()
122
+
123
+ provider = Provider(DummyAuth(None))
124
+
125
+ with pytest.raises(PlaceFulfillmentError):
126
+ asyncio.run(provider.discover())
127
+
128
+
129
+ def test_provider_enable_raises_on_success_false() -> None:
130
+ payload = {"success": False, "message": "not authorized"}
131
+
132
+ class DummyAuth(AbstractAuth):
133
+ async def async_get_access_token(self) -> str:
134
+ return "token"
135
+
136
+ async def request(self, method, url, **kwargs):
137
+ class DummyResponse:
138
+ async def json(self_inner):
139
+ return payload
140
+
141
+ return DummyResponse()
142
+
143
+ provider = Provider(DummyAuth(None))
144
+
145
+ with pytest.raises(PlaceFulfillmentError):
146
+ asyncio.run(provider.enable())
147
+
148
+
149
+ def test_provider_raises_on_non_dict_response() -> None:
150
+ class DummyAuth(AbstractAuth):
151
+ async def async_get_access_token(self) -> str:
152
+ return "token"
153
+
154
+ async def request(self, method, url, **kwargs):
155
+ class DummyResponse:
156
+ async def json(self_inner):
157
+ return None
158
+
159
+ return DummyResponse()
160
+
161
+ provider = Provider(DummyAuth(None))
162
+
163
+ with pytest.raises(PlaceFulfillmentError):
164
+ asyncio.run(provider.discover())
165
+
166
+
167
+ def test_provider_raises_on_invalid_json_body() -> None:
168
+ class DummyAuth(AbstractAuth):
169
+ async def async_get_access_token(self) -> str:
170
+ return "token"
171
+
172
+ async def request(self, method, url, **kwargs):
173
+ class DummyResponse:
174
+ async def json(self_inner):
175
+ raise ValueError("Expecting value: line 1 column 1 (char 0)")
176
+
177
+ return DummyResponse()
178
+
179
+ provider = Provider(DummyAuth(None))
180
+
181
+ with pytest.raises(PlaceFulfillmentError):
182
+ asyncio.run(provider.discover())
@@ -1,3 +0,0 @@
1
- # Place integration API
2
-
3
- This project contains a python API to connect MQTT data from Place devices to Place's Home Assistant integration
@@ -1,38 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from .auth.abstract_auth import AbstractAuth
4
- from .models.discover_device import DiscoverDevice
5
- from .config import FULFILLMENT_URL
6
-
7
- class Provider:
8
- def __init__(self, authorized_session: AbstractAuth) -> None:
9
- self.authorized_session = authorized_session
10
-
11
-
12
- async def discover(self) -> list[DiscoverDevice]:
13
- body = {"command": "DISCOVER", "data": {}}
14
- resp = await self.authorized_session.request("POST", FULFILLMENT_URL, json=body)
15
- data = await resp.json()
16
- if not data.get("success", True):
17
- raise RuntimeError(f"Home Assistant error: {data.get('message', data)}")
18
- devices_raw = (data.get("data") or {}).get("devices") or []
19
- devices: list[DiscoverDevice] = []
20
- for raw in devices_raw:
21
- devices.append(DiscoverDevice.from_dict(raw))
22
- return devices
23
-
24
- async def enable(self):
25
- body = {"command": "ENABLE", "data": {}}
26
- resp = await self.authorized_session.request("POST", FULFILLMENT_URL, json=body)
27
- data = await resp.json()
28
- if not data.get("success", True):
29
- raise RuntimeError(f"Home Assistant error: {data.get('message', data)}")
30
- return data
31
-
32
- async def disable(self):
33
- body = {"command": "DISABLE", "data": {}}
34
- resp = await self.authorized_session.request("POST", FULFILLMENT_URL, json=body)
35
- data = await resp.json()
36
- if not data.get("success", True):
37
- raise RuntimeError(f"Home Assistant error: {data.get('message', data)}")
38
- return data
@@ -1,31 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from unittest.mock import MagicMock, patch
4
- from place.auth import get_iot_credentials
5
-
6
-
7
- @patch("place.auth.srp_auth.boto3")
8
- def test_get_iot_credentials_success(mock_boto3: MagicMock) -> None:
9
- identity_client = MagicMock()
10
- mock_boto3.client.return_value = identity_client
11
-
12
- identity_client.get_id.return_value = {"IdentityId": "identity-123"}
13
- identity_client.get_credentials_for_identity.return_value = {
14
- "Credentials": {
15
- "AccessKeyId": "AKIA...",
16
- "SecretKey": "secret",
17
- "SessionToken": "session",
18
- }
19
- }
20
-
21
-
22
- creds = get_iot_credentials(
23
- id_token="id-token",
24
- access_token="access-token",
25
- )
26
-
27
- assert creds.access_key_id == "AKIA..."
28
- assert creds.secret_access_key == "secret"
29
- assert creds.session_token == "session"
30
- assert creds.identity_id == "identity-123"
31
- assert creds.access_token == "access-token"