python-casahome 0.2.0__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 (60) hide show
  1. python_casahome-0.2.0/.gitignore +16 -0
  2. python_casahome-0.2.0/.python-version +1 -0
  3. python_casahome-0.2.0/LICENSE +21 -0
  4. python_casahome-0.2.0/PKG-INFO +272 -0
  5. python_casahome-0.2.0/README.md +256 -0
  6. python_casahome-0.2.0/examples/bootstrap.py +53 -0
  7. python_casahome-0.2.0/pyproject.toml +50 -0
  8. python_casahome-0.2.0/src/casahome/__init__.py +30 -0
  9. python_casahome-0.2.0/src/casahome/_base_http.py +21 -0
  10. python_casahome-0.2.0/src/casahome/api.py +1135 -0
  11. python_casahome-0.2.0/src/casahome/auth.py +199 -0
  12. python_casahome-0.2.0/src/casahome/client.py +168 -0
  13. python_casahome-0.2.0/src/casahome/const.py +34 -0
  14. python_casahome-0.2.0/src/casahome/exceptions.py +52 -0
  15. python_casahome-0.2.0/src/casahome/models/__init__.py +477 -0
  16. python_casahome-0.2.0/src/casahome/models/_base.py +25 -0
  17. python_casahome-0.2.0/src/casahome/models/ai.py +58 -0
  18. python_casahome-0.2.0/src/casahome/models/assets.py +123 -0
  19. python_casahome-0.2.0/src/casahome/models/availability.py +39 -0
  20. python_casahome-0.2.0/src/casahome/models/bootstrap.py +134 -0
  21. python_casahome-0.2.0/src/casahome/models/calendar.py +33 -0
  22. python_casahome-0.2.0/src/casahome/models/catalogs.py +73 -0
  23. python_casahome-0.2.0/src/casahome/models/credits.py +29 -0
  24. python_casahome-0.2.0/src/casahome/models/documents.py +127 -0
  25. python_casahome-0.2.0/src/casahome/models/enums.py +183 -0
  26. python_casahome-0.2.0/src/casahome/models/feed.py +30 -0
  27. python_casahome-0.2.0/src/casahome/models/help_center.py +45 -0
  28. python_casahome-0.2.0/src/casahome/models/home.py +175 -0
  29. python_casahome-0.2.0/src/casahome/models/images.py +48 -0
  30. python_casahome-0.2.0/src/casahome/models/mailbox.py +160 -0
  31. python_casahome-0.2.0/src/casahome/models/maintenance.py +200 -0
  32. python_casahome-0.2.0/src/casahome/models/membership.py +65 -0
  33. python_casahome-0.2.0/src/casahome/models/paint.py +56 -0
  34. python_casahome-0.2.0/src/casahome/models/preferences.py +104 -0
  35. python_casahome-0.2.0/src/casahome/models/purchases.py +106 -0
  36. python_casahome-0.2.0/src/casahome/models/read_todos.py +22 -0
  37. python_casahome-0.2.0/src/casahome/models/referral.py +21 -0
  38. python_casahome-0.2.0/src/casahome/models/requests.py +149 -0
  39. python_casahome-0.2.0/src/casahome/models/tasks.py +77 -0
  40. python_casahome-0.2.0/src/casahome/models/taxes.py +47 -0
  41. python_casahome-0.2.0/src/casahome/models/todos.py +52 -0
  42. python_casahome-0.2.0/src/casahome/models/visits.py +158 -0
  43. python_casahome-0.2.0/src/casahome/models/write_contracts.py +147 -0
  44. python_casahome-0.2.0/src/casahome/py.typed +0 -0
  45. python_casahome-0.2.0/src/casahome/streaming.py +278 -0
  46. python_casahome-0.2.0/tests/__init__.py +0 -0
  47. python_casahome-0.2.0/tests/conftest.py +117 -0
  48. python_casahome-0.2.0/tests/fixtures/bootstrap.json +77 -0
  49. python_casahome-0.2.0/tests/fixtures/error.json +8 -0
  50. python_casahome-0.2.0/tests/fixtures/token_refresh.json +9 -0
  51. python_casahome-0.2.0/tests/test_api.py +236 -0
  52. python_casahome-0.2.0/tests/test_auth.py +172 -0
  53. python_casahome-0.2.0/tests/test_client.py +170 -0
  54. python_casahome-0.2.0/tests/test_inventory_models.py +189 -0
  55. python_casahome-0.2.0/tests/test_models.py +64 -0
  56. python_casahome-0.2.0/tests/test_read_coverage.py +1019 -0
  57. python_casahome-0.2.0/tests/test_static_reads.py +810 -0
  58. python_casahome-0.2.0/tests/test_streaming.py +883 -0
  59. python_casahome-0.2.0/tests/test_write_contracts.py +999 -0
  60. python_casahome-0.2.0/uv.lock +1297 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ .env
12
+ .env.*
13
+ !.env.example
14
+ *.har
15
+ traffic/
16
+ *.local.*
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Borski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-casahome
3
+ Version: 0.2.0
4
+ Summary: Unofficial async client for the Casa (getcasa.com) member API
5
+ Author: Borski
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: aiohttp>=3.9
14
+ Requires-Dist: pydantic>=2.5
15
+ Description-Content-Type: text/markdown
16
+
17
+ # python-casahome
18
+
19
+ Async Python client for the [Casa](https://getcasa.com) home membership API.
20
+
21
+ Unofficial. Not affiliated with or endorsed by Casa.
22
+
23
+ ## Status
24
+
25
+ Version 0.2.0, alpha.
26
+
27
+ - The library is read-heavy. Most methods are GETs, and most of those have been
28
+ confirmed against live traffic.
29
+ - A few write methods exist. They are unverified and haven't been exercised
30
+ against the real backend. Treat them as experimental.
31
+ - Endpoint shapes were inferred from the iOS app and confirmed against live
32
+ traffic where marked. Casa can change its private API at any time.
33
+
34
+ ## Install
35
+
36
+ PyPI isn't set up yet. Install from GitHub:
37
+
38
+ ```
39
+ pip install "python-casahome @ git+https://github.com/borski/python-casahome@v0.2.0"
40
+ ```
41
+
42
+ Requires Python 3.11 or newer.
43
+
44
+ ## Authentication
45
+
46
+ Casa signs members in with Firebase phone auth, and SMS sign-in only works from
47
+ the real app. This library starts from a Firebase refresh token instead and
48
+ exchanges it for short-lived ID tokens.
49
+
50
+ Casa offers no official way to export that token. You obtain your own refresh
51
+ token from your own Casa app session, for example by inspecting your own app's
52
+ traffic during login and reading `refreshToken` from the Firebase token
53
+ response.
54
+
55
+ Treat the refresh token like a password. Anyone holding it can act as you.
56
+ Keep it in a password manager or secret store, never in source control.
57
+
58
+ The token doesn't expire on a timer, but Firebase can rotate it. Pass
59
+ `on_refresh_token_rotated` to `FirebaseAuth` (sync or async) and persist the new
60
+ value when it fires.
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ import asyncio
66
+
67
+ from casahome import CasaApi
68
+
69
+
70
+ async def main() -> None:
71
+ async with CasaApi.create("YOUR_REFRESH_TOKEN") as api:
72
+ me = await api.bootstrap()
73
+ for home in me.homes:
74
+ print(home.display_address, home.status)
75
+ weather = await api.weather(home.id)
76
+ print(weather)
77
+
78
+
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ If you already own an `aiohttp.ClientSession` (for example in Home Assistant),
83
+ build the pieces yourself:
84
+
85
+ ```python
86
+ from casahome import CasaApi, CasaClient, FirebaseAuth
87
+
88
+ auth = FirebaseAuth(session, refresh_token, on_refresh_token_rotated=save_token)
89
+ api = CasaApi(
90
+ CasaClient(session, auth),
91
+ device_id=stored_device_id,
92
+ timezone="America/Los_Angeles",
93
+ )
94
+ ```
95
+
96
+ `bootstrap` registers a device with Casa. Long-lived callers should persist
97
+ `api.device_id` and pass it back on the next start. Otherwise every restart adds
98
+ a new device to the account.
99
+
100
+ A runnable example lives in `examples/bootstrap.py`:
101
+
102
+ ```
103
+ CASA_REFRESH_TOKEN=... uv run examples/bootstrap.py
104
+ ```
105
+
106
+ ## Coverage
107
+
108
+ Each method's docstring starts with `VERIFIED` or `UNVERIFIED`.
109
+ `casahome.is_unverified(api.some_method)` reports the same thing at runtime.
110
+
111
+ The "Live-confirmed" column means:
112
+
113
+ - **yes**: the path, method, response envelope and list item shapes were seen
114
+ in live traffic.
115
+ - **partial**: the path, method and envelope were seen, but lists came back
116
+ empty or items are kept as opaque JSON. Item models are inferred.
117
+ - **no**: inferred from the app only. Not seen working live.
118
+
119
+ The envelope is the top-level response keys. Item shape is the structure of
120
+ elements inside lists. All live confirmation dates from 2026-10.
121
+
122
+ ### Reads
123
+
124
+ | Method | Live-confirmed | Notes |
125
+ |---|---|---|
126
+ | `bootstrap` | yes | Profile, homes, badge counts, app update info. |
127
+ | `onboarding_details` | yes | Raw dict, or `None` on 304. |
128
+ | `onboarding_details_typed` | yes | Typed version of the above. |
129
+ | `onboarding_availability` | yes | |
130
+ | `home_details` | yes | Stories, rooms, inventory entities. |
131
+ | `inventory_item` | yes | One entity. |
132
+ | `inventory_makes` | yes | |
133
+ | `inventory_enums` | yes | Takes a property's `enum_type` key, such as `light_fixture_type`. |
134
+ | `inventory_paint_matches` | yes | |
135
+ | `inventory_service_history` | partial | Room and home targets seen. Entity target untested. |
136
+ | `room_types` | yes | |
137
+ | `room_image_options` | yes | |
138
+ | `room_image_candidates` | yes | |
139
+ | `matterport_images` | yes | |
140
+ | `badge_counts` | yes | |
141
+ | `weather` | yes | |
142
+ | `credits_summary` | yes | |
143
+ | `home_feed` | yes | `props` varies by card template and stays a JSON dict. |
144
+ | `home_members` | yes | |
145
+ | `schedule_preferences` | yes | |
146
+ | `notification_settings` | yes | |
147
+ | `access_preferences` | partial | Option values not seen populated. |
148
+ | `autopilot_plans` | yes | |
149
+ | `paint_brands`, `paint_collections`, `paint_colors`, `paint_search` | yes | |
150
+ | `suggested_todos` | yes | Question rounds and destination stay opaque. |
151
+ | `todo_details` | yes | Same caveat. |
152
+ | `document_types` | yes | |
153
+ | `help_center_bundle` | yes | Only `member_ios` tested. |
154
+ | `release_notes` | yes | |
155
+ | `release_notes_diff` | yes | |
156
+ | `casa_ai_bootstrap` | partial | Messages not seen populated. |
157
+ | `casa_ai_messages` | partial | |
158
+ | `requests_tab` | partial | |
159
+ | `completed_requests` | partial | |
160
+ | `calendar` | partial | Bounds sent as epoch seconds. |
161
+ | `calendar_busy` | partial | |
162
+ | `calendar_sync` | partial | |
163
+ | `mailbox`, `mail_items`, `packages` | partial | |
164
+ | `documents` | partial | Query parameters untested. |
165
+ | `work_document_source` | partial | Only the `todo_id` selector tested. |
166
+ | `vehicles` | partial | |
167
+ | `utilities_insights` | partial | |
168
+ | `purchases` | partial | |
169
+ | `property_taxes` | partial | |
170
+ | `recurring_vendor_categories` | yes | |
171
+ | `recurring_vendors` | partial | |
172
+ | `bookable_handyman_tasks` | partial | Items stay opaque JSON. |
173
+ | `referral_code` | partial | `display` and `referred_homes` stay opaque JSON. |
174
+ | `request_details` | no | |
175
+ | `membership_info` | no | Returned 404 in testing. |
176
+ | `membership_savings` | no | Returned 404 in testing. |
177
+ | `availability_request_details` | no | |
178
+ | `document_details` | no | |
179
+ | `document_suggestions` | no | |
180
+ | `document_structure_suggestions` | no | |
181
+ | `request_purchases` | no | |
182
+ | `task_details` | no | |
183
+ | `vendor_visit_details` | no | |
184
+ | `vendor_visit_autopilot_plan` | no | |
185
+ | `vendor_visit_calls` | no | |
186
+ | `vendor_visit_conversation` | no | |
187
+ | `vendor_visit_purchases` | no | |
188
+ | `casa_ai_events` | partial | Opt-in SSE stream. Transport confirmed, event payloads untested. |
189
+
190
+ ### Writes
191
+
192
+ | Method | Live-confirmed |
193
+ |---|---|
194
+ | `create_todo_result` | no |
195
+ | `create_todo` | no |
196
+ | `send_request_message` | no |
197
+ | `update_entity_properties` | no |
198
+
199
+ ## Models
200
+
201
+ Import models from `casahome.models`. Unknown response fields are kept in
202
+ `model_extra` rather than dropped, so new server fields don't break parsing.
203
+ A response that doesn't match the expected shape raises `CasaResponseError`.
204
+
205
+ Identifier arguments are checked before any request. They must be nonempty
206
+ path-safe segments (letters, digits, `.`, `_`, `~`, `-`). IDs aren't trimmed,
207
+ lowercased or forced to UUIDs.
208
+
209
+ Responses can contain personal data: names, contact details, messages,
210
+ documents and purchase info. Avoid logging whole models. URLs in responses are
211
+ returned as data. The library never follows them.
212
+
213
+ ## Writes
214
+
215
+ The write methods are unverified. Their request bodies were inferred from the
216
+ app and have not been exercised against the live backend. Nothing is sent until
217
+ you await a write method.
218
+
219
+ - Each write is sent exactly once. There is no retry, including on 401. The ID
220
+ token is refreshed before the request, and a 401 raises `CasaAuthError`.
221
+ Deciding whether to repeat is up to you.
222
+ - If the response fails to parse, the server may still have performed the
223
+ action. Don't assume a parse error means it's safe to repeat.
224
+ - Request bodies are strict Pydantic models. Unknown fields and invalid values
225
+ raise `CasaRequestError` before any I/O. Free-text fields are capped at
226
+ 10,000 characters.
227
+ - Optional fields set to `None` are omitted from the JSON. Empty strings and
228
+ empty lists are sent as given.
229
+ - `send_request_message` only allows the `member` sender role.
230
+ - `create_todo_result` returns the full envelope, including
231
+ `needs_support_confirmation`. `create_todo` is the older helper that returns
232
+ just the todo.
233
+
234
+ ## CasaAI event stream
235
+
236
+ `api.casa_ai_events(home_id, after=None)` is an opt-in async generator over the
237
+ CasaAI server-sent event stream. It doesn't reconnect on its own. Close it with
238
+ `contextlib.aclosing` if you stop early:
239
+
240
+ ```python
241
+ from contextlib import aclosing
242
+
243
+ from casahome.streaming import CasaAIResyncRequiredEvent, CasaAIThreadUpdateEvent
244
+
245
+ async with aclosing(api.casa_ai_events(home_id, after=saved_cursor)) as events:
246
+ async for event in events:
247
+ saved_cursor = event.id
248
+ if isinstance(event, CasaAIResyncRequiredEvent):
249
+ break # Reload state, then subscribe again.
250
+ if isinstance(event, CasaAIThreadUpdateEvent):
251
+ update = event.payload # Opaque JSON.
252
+ ```
253
+
254
+ Lines are capped at 64 KiB and events at 256 KiB. A 401 refreshes the token and
255
+ retries the GET once.
256
+
257
+ ## Development
258
+
259
+ ```
260
+ uv sync
261
+ uv run pytest -q
262
+ uv run ruff check .
263
+ uv run ruff format --check .
264
+ uv run mypy src tests
265
+ ```
266
+
267
+ Tests use synthetic data and a loopback test server. They make no network calls
268
+ and need no credentials.
269
+
270
+ ## License
271
+
272
+ MIT. See `LICENSE`.
@@ -0,0 +1,256 @@
1
+ # python-casahome
2
+
3
+ Async Python client for the [Casa](https://getcasa.com) home membership API.
4
+
5
+ Unofficial. Not affiliated with or endorsed by Casa.
6
+
7
+ ## Status
8
+
9
+ Version 0.2.0, alpha.
10
+
11
+ - The library is read-heavy. Most methods are GETs, and most of those have been
12
+ confirmed against live traffic.
13
+ - A few write methods exist. They are unverified and haven't been exercised
14
+ against the real backend. Treat them as experimental.
15
+ - Endpoint shapes were inferred from the iOS app and confirmed against live
16
+ traffic where marked. Casa can change its private API at any time.
17
+
18
+ ## Install
19
+
20
+ PyPI isn't set up yet. Install from GitHub:
21
+
22
+ ```
23
+ pip install "python-casahome @ git+https://github.com/borski/python-casahome@v0.2.0"
24
+ ```
25
+
26
+ Requires Python 3.11 or newer.
27
+
28
+ ## Authentication
29
+
30
+ Casa signs members in with Firebase phone auth, and SMS sign-in only works from
31
+ the real app. This library starts from a Firebase refresh token instead and
32
+ exchanges it for short-lived ID tokens.
33
+
34
+ Casa offers no official way to export that token. You obtain your own refresh
35
+ token from your own Casa app session, for example by inspecting your own app's
36
+ traffic during login and reading `refreshToken` from the Firebase token
37
+ response.
38
+
39
+ Treat the refresh token like a password. Anyone holding it can act as you.
40
+ Keep it in a password manager or secret store, never in source control.
41
+
42
+ The token doesn't expire on a timer, but Firebase can rotate it. Pass
43
+ `on_refresh_token_rotated` to `FirebaseAuth` (sync or async) and persist the new
44
+ value when it fires.
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ import asyncio
50
+
51
+ from casahome import CasaApi
52
+
53
+
54
+ async def main() -> None:
55
+ async with CasaApi.create("YOUR_REFRESH_TOKEN") as api:
56
+ me = await api.bootstrap()
57
+ for home in me.homes:
58
+ print(home.display_address, home.status)
59
+ weather = await api.weather(home.id)
60
+ print(weather)
61
+
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ If you already own an `aiohttp.ClientSession` (for example in Home Assistant),
67
+ build the pieces yourself:
68
+
69
+ ```python
70
+ from casahome import CasaApi, CasaClient, FirebaseAuth
71
+
72
+ auth = FirebaseAuth(session, refresh_token, on_refresh_token_rotated=save_token)
73
+ api = CasaApi(
74
+ CasaClient(session, auth),
75
+ device_id=stored_device_id,
76
+ timezone="America/Los_Angeles",
77
+ )
78
+ ```
79
+
80
+ `bootstrap` registers a device with Casa. Long-lived callers should persist
81
+ `api.device_id` and pass it back on the next start. Otherwise every restart adds
82
+ a new device to the account.
83
+
84
+ A runnable example lives in `examples/bootstrap.py`:
85
+
86
+ ```
87
+ CASA_REFRESH_TOKEN=... uv run examples/bootstrap.py
88
+ ```
89
+
90
+ ## Coverage
91
+
92
+ Each method's docstring starts with `VERIFIED` or `UNVERIFIED`.
93
+ `casahome.is_unverified(api.some_method)` reports the same thing at runtime.
94
+
95
+ The "Live-confirmed" column means:
96
+
97
+ - **yes**: the path, method, response envelope and list item shapes were seen
98
+ in live traffic.
99
+ - **partial**: the path, method and envelope were seen, but lists came back
100
+ empty or items are kept as opaque JSON. Item models are inferred.
101
+ - **no**: inferred from the app only. Not seen working live.
102
+
103
+ The envelope is the top-level response keys. Item shape is the structure of
104
+ elements inside lists. All live confirmation dates from 2026-10.
105
+
106
+ ### Reads
107
+
108
+ | Method | Live-confirmed | Notes |
109
+ |---|---|---|
110
+ | `bootstrap` | yes | Profile, homes, badge counts, app update info. |
111
+ | `onboarding_details` | yes | Raw dict, or `None` on 304. |
112
+ | `onboarding_details_typed` | yes | Typed version of the above. |
113
+ | `onboarding_availability` | yes | |
114
+ | `home_details` | yes | Stories, rooms, inventory entities. |
115
+ | `inventory_item` | yes | One entity. |
116
+ | `inventory_makes` | yes | |
117
+ | `inventory_enums` | yes | Takes a property's `enum_type` key, such as `light_fixture_type`. |
118
+ | `inventory_paint_matches` | yes | |
119
+ | `inventory_service_history` | partial | Room and home targets seen. Entity target untested. |
120
+ | `room_types` | yes | |
121
+ | `room_image_options` | yes | |
122
+ | `room_image_candidates` | yes | |
123
+ | `matterport_images` | yes | |
124
+ | `badge_counts` | yes | |
125
+ | `weather` | yes | |
126
+ | `credits_summary` | yes | |
127
+ | `home_feed` | yes | `props` varies by card template and stays a JSON dict. |
128
+ | `home_members` | yes | |
129
+ | `schedule_preferences` | yes | |
130
+ | `notification_settings` | yes | |
131
+ | `access_preferences` | partial | Option values not seen populated. |
132
+ | `autopilot_plans` | yes | |
133
+ | `paint_brands`, `paint_collections`, `paint_colors`, `paint_search` | yes | |
134
+ | `suggested_todos` | yes | Question rounds and destination stay opaque. |
135
+ | `todo_details` | yes | Same caveat. |
136
+ | `document_types` | yes | |
137
+ | `help_center_bundle` | yes | Only `member_ios` tested. |
138
+ | `release_notes` | yes | |
139
+ | `release_notes_diff` | yes | |
140
+ | `casa_ai_bootstrap` | partial | Messages not seen populated. |
141
+ | `casa_ai_messages` | partial | |
142
+ | `requests_tab` | partial | |
143
+ | `completed_requests` | partial | |
144
+ | `calendar` | partial | Bounds sent as epoch seconds. |
145
+ | `calendar_busy` | partial | |
146
+ | `calendar_sync` | partial | |
147
+ | `mailbox`, `mail_items`, `packages` | partial | |
148
+ | `documents` | partial | Query parameters untested. |
149
+ | `work_document_source` | partial | Only the `todo_id` selector tested. |
150
+ | `vehicles` | partial | |
151
+ | `utilities_insights` | partial | |
152
+ | `purchases` | partial | |
153
+ | `property_taxes` | partial | |
154
+ | `recurring_vendor_categories` | yes | |
155
+ | `recurring_vendors` | partial | |
156
+ | `bookable_handyman_tasks` | partial | Items stay opaque JSON. |
157
+ | `referral_code` | partial | `display` and `referred_homes` stay opaque JSON. |
158
+ | `request_details` | no | |
159
+ | `membership_info` | no | Returned 404 in testing. |
160
+ | `membership_savings` | no | Returned 404 in testing. |
161
+ | `availability_request_details` | no | |
162
+ | `document_details` | no | |
163
+ | `document_suggestions` | no | |
164
+ | `document_structure_suggestions` | no | |
165
+ | `request_purchases` | no | |
166
+ | `task_details` | no | |
167
+ | `vendor_visit_details` | no | |
168
+ | `vendor_visit_autopilot_plan` | no | |
169
+ | `vendor_visit_calls` | no | |
170
+ | `vendor_visit_conversation` | no | |
171
+ | `vendor_visit_purchases` | no | |
172
+ | `casa_ai_events` | partial | Opt-in SSE stream. Transport confirmed, event payloads untested. |
173
+
174
+ ### Writes
175
+
176
+ | Method | Live-confirmed |
177
+ |---|---|
178
+ | `create_todo_result` | no |
179
+ | `create_todo` | no |
180
+ | `send_request_message` | no |
181
+ | `update_entity_properties` | no |
182
+
183
+ ## Models
184
+
185
+ Import models from `casahome.models`. Unknown response fields are kept in
186
+ `model_extra` rather than dropped, so new server fields don't break parsing.
187
+ A response that doesn't match the expected shape raises `CasaResponseError`.
188
+
189
+ Identifier arguments are checked before any request. They must be nonempty
190
+ path-safe segments (letters, digits, `.`, `_`, `~`, `-`). IDs aren't trimmed,
191
+ lowercased or forced to UUIDs.
192
+
193
+ Responses can contain personal data: names, contact details, messages,
194
+ documents and purchase info. Avoid logging whole models. URLs in responses are
195
+ returned as data. The library never follows them.
196
+
197
+ ## Writes
198
+
199
+ The write methods are unverified. Their request bodies were inferred from the
200
+ app and have not been exercised against the live backend. Nothing is sent until
201
+ you await a write method.
202
+
203
+ - Each write is sent exactly once. There is no retry, including on 401. The ID
204
+ token is refreshed before the request, and a 401 raises `CasaAuthError`.
205
+ Deciding whether to repeat is up to you.
206
+ - If the response fails to parse, the server may still have performed the
207
+ action. Don't assume a parse error means it's safe to repeat.
208
+ - Request bodies are strict Pydantic models. Unknown fields and invalid values
209
+ raise `CasaRequestError` before any I/O. Free-text fields are capped at
210
+ 10,000 characters.
211
+ - Optional fields set to `None` are omitted from the JSON. Empty strings and
212
+ empty lists are sent as given.
213
+ - `send_request_message` only allows the `member` sender role.
214
+ - `create_todo_result` returns the full envelope, including
215
+ `needs_support_confirmation`. `create_todo` is the older helper that returns
216
+ just the todo.
217
+
218
+ ## CasaAI event stream
219
+
220
+ `api.casa_ai_events(home_id, after=None)` is an opt-in async generator over the
221
+ CasaAI server-sent event stream. It doesn't reconnect on its own. Close it with
222
+ `contextlib.aclosing` if you stop early:
223
+
224
+ ```python
225
+ from contextlib import aclosing
226
+
227
+ from casahome.streaming import CasaAIResyncRequiredEvent, CasaAIThreadUpdateEvent
228
+
229
+ async with aclosing(api.casa_ai_events(home_id, after=saved_cursor)) as events:
230
+ async for event in events:
231
+ saved_cursor = event.id
232
+ if isinstance(event, CasaAIResyncRequiredEvent):
233
+ break # Reload state, then subscribe again.
234
+ if isinstance(event, CasaAIThreadUpdateEvent):
235
+ update = event.payload # Opaque JSON.
236
+ ```
237
+
238
+ Lines are capped at 64 KiB and events at 256 KiB. A 401 refreshes the token and
239
+ retries the GET once.
240
+
241
+ ## Development
242
+
243
+ ```
244
+ uv sync
245
+ uv run pytest -q
246
+ uv run ruff check .
247
+ uv run ruff format --check .
248
+ uv run mypy src tests
249
+ ```
250
+
251
+ Tests use synthetic data and a loopback test server. They make no network calls
252
+ and need no credentials.
253
+
254
+ ## License
255
+
256
+ MIT. See `LICENSE`.
@@ -0,0 +1,53 @@
1
+ """Print a summary of your Casa account.
2
+
3
+ CASA_REFRESH_TOKEN=... uv run examples/bootstrap.py [--show-token]
4
+ op run --env-file=.env -- uv run examples/bootstrap.py
5
+
6
+ Only calls the Firebase token endpoint and the VERIFIED bootstrap endpoint.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import asyncio
13
+ import os
14
+ import sys
15
+
16
+ from casahome import CasaApi, CasaError
17
+
18
+
19
+ async def main(show_token: bool) -> int:
20
+ token = os.environ.get("CASA_REFRESH_TOKEN", "").strip()
21
+ if not token:
22
+ print("Set CASA_REFRESH_TOKEN", file=sys.stderr)
23
+ return 2
24
+ try:
25
+ async with CasaApi.create(token) as api:
26
+ me = await api.bootstrap()
27
+ rotated = api.client.auth.refresh_token
28
+ except CasaError as err:
29
+ print(f"{type(err).__name__}: {err}", file=sys.stderr)
30
+ return 1
31
+
32
+ print(f"Hi {me.first_name}. {len(me.homes)} home(s).")
33
+ for home in me.homes:
34
+ print(f" {home.id}: status={home.status} plan_state={home.plan_state}")
35
+ if home.badge_counts:
36
+ print(f" badges: {home.badge_counts.features} (total {home.badge_counts.total})")
37
+ if home.pending_agreements:
38
+ names = ", ".join(a.name for a in home.pending_agreements)
39
+ print(f" pending agreements: {names}")
40
+ if me.app_update:
41
+ print(f"App update required: {me.app_update.force_update_required}")
42
+ if show_token:
43
+ changed = "rotated" if rotated != token else "unchanged"
44
+ print(f"Refresh token ({changed}): {rotated}")
45
+ elif rotated != token:
46
+ print("Note: Firebase rotated the refresh token. Rerun with --show-token to see it.")
47
+ return 0
48
+
49
+
50
+ if __name__ == "__main__":
51
+ parser = argparse.ArgumentParser(description=__doc__)
52
+ parser.add_argument("--show-token", action="store_true")
53
+ sys.exit(asyncio.run(main(parser.parse_args().show_token)))
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "python-casahome"
3
+ version = "0.2.0"
4
+ description = "Unofficial async client for the Casa (getcasa.com) member API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Borski" }]
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "aiohttp>=3.9",
11
+ "pydantic>=2.5",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Framework :: AsyncIO",
16
+ "Programming Language :: Python :: 3",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "mypy>=1.10",
23
+ "pytest>=8",
24
+ "pytest-asyncio>=0.23",
25
+ "ruff>=0.6",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/casahome"]
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ target-version = "py311"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
41
+
42
+ [tool.mypy]
43
+ strict = true
44
+ python_version = "3.11"
45
+ plugins = ["pydantic.mypy"]
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ asyncio_default_fixture_loop_scope = "function"
50
+ testpaths = ["tests"]