stoop 0.1.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.
stoop-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uladzislau Bayouski
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.
stoop-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: stoop
3
+ Version: 0.1.0
4
+ Summary: Turn front-door events (Ring and others) into decisions a person can act on: memory, routines, policy, and human-gated actions.
5
+ Keywords: ring,doorbell,caregiving,agent,mcp,bedrock
6
+ Author: Uladzislau Bayouski
7
+ Author-email: Uladzislau Bayouski <uladzislaubayouski@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: pydantic>=2
21
+ Requires-Dist: tzdata>=2025.1 ; sys_platform == 'win32'
22
+ Requires-Dist: boto3>=1.40 ; extra == 'aws'
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/iot-forge/Stoop
25
+ Project-URL: Repository, https://github.com/iot-forge/Stoop
26
+ Project-URL: Issues, https://github.com/iot-forge/Stoop/issues
27
+ Project-URL: Changelog, https://github.com/iot-forge/Stoop/blob/main/CHANGELOG.md
28
+ Provides-Extra: aws
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Stoop
32
+
33
+ [![CI](https://github.com/iot-forge/Stoop/actions/workflows/ci.yml/badge.svg)](https://github.com/iot-forge/Stoop/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/stoop.svg)](https://pypi.org/project/stoop/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/stoop.svg)](https://pypi.org/project/stoop/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
37
+
38
+ Turn front-door events into decisions a person can act on.
39
+
40
+ `stoop` is a small Python library that sits between a doorbell or camera feed (Ring today,
41
+ anything tomorrow) and the humans who care about what happens at that door. It remembers who
42
+ is expected, learns what is normal for that door, decides whether an event should be
43
+ ignored, logged, sent as a notification, or escalated, and keeps every sensitive action behind
44
+ a human confirmation.
45
+
46
+ It was built during the [Build, Ship, Shape: Amazon Developer Hackathon](https://amazonappdev2026.devpost.com/)
47
+ as the shared core of two products: a caregiving app on the Ring track and a conversational
48
+ property concierge on the Alexa+ track. The library is product-agnostic and MIT licensed.
49
+
50
+ ## What it does
51
+
52
+ ```
53
+ Ring webhook / history ──┐
54
+ Synthetic scenarios ─────┼─▶ Event ─▶ Store (SQLite) ─▶ PolicyEngine ─▶ Decision ─▶ Sinks
55
+ Your own source ─────────┘ ▲ ▲ │
56
+ │ └── RoutineModel ─┘ (what is normal here?)
57
+ └───── ExpectedVisit / Person (who is expected?)
58
+ ```
59
+
60
+ - **Events** (`stoop.events`): one normalized shape for motion, doorbell presses, door
61
+ sensors, device health and environmental sensors. Ring's `X-Signature` HMAC is verified
62
+ over the raw body.
63
+ - **Memory** (`stoop.memory`): sites, people and their roles, expected visits (one-off or
64
+ recurring), visits (events clustered into "someone came to the door"), decisions, and a
65
+ small key/value state. SQLite, standard library only.
66
+ - **Routines** (`stoop.policy.routines`): weekly rates per weekday and hour, an anomaly
67
+ score for "how surprising is this right now", and "does this door usually see someone
68
+ every day".
69
+ - **Policy** (`stoop.policy.engine`): deterministic rules that always produce a complete
70
+ decision, plus a sweep for things that did not happen: no-shows, inactivity, doors left
71
+ open. Notifications about the same visit are suppressed so one visitor is one alert.
72
+ - **Reasoning** (`stoop.reasoning`): an optional refinement step. The deterministic reasoner
73
+ adds context with no model call. The Bedrock reasoner uses the Converse API (Amazon Nova 2
74
+ Lite by default, multimodal when a snapshot is available). A reasoner can reword and move
75
+ severity by one step, and can never remove a confirmation requirement.
76
+ - **Pipeline** (`stoop.pipeline`): source → engine → sinks, with backfill for routine learning.
77
+
78
+ Reasoning runs on the ingestion path only. Anything that answers a question (a dashboard, an
79
+ MCP tool for Alexa+) reads precomputed state and stays fast.
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install stoop # core
85
+ pip install "stoop[aws]" # + boto3 for the Bedrock reasoner
86
+ ```
87
+
88
+ Python 3.13 or newer.
89
+
90
+ ## Quick start
91
+
92
+ ```python
93
+ from datetime import UTC, datetime, time
94
+
95
+ from stoop import ExpectedVisit, Person, Pipeline, PolicyEngine, Role, Site, Store, LogSink
96
+ from stoop.sources.synthetic import BUILTIN, generate_baseline, play_scenario
97
+
98
+ store = Store("stoop.db")
99
+ site = store.put_site(Site(id="moms-house", name="Mom's house", timezone="America/New_York"))
100
+ maria = store.put_person(Person(site_id=site.id, name="Maria", role=Role.AIDE))
101
+ store.put_expected(ExpectedVisit(site_id=site.id, label="Morning aide", person_id=maria.id,
102
+ days_of_week=[0, 2, 4], local_start=time(9, 0), local_end=time(10, 30)))
103
+
104
+ pipeline = Pipeline(PolicyEngine(store), sinks=[LogSink()])
105
+
106
+ # Teach it what "normal" looks like (synthetic here; real history from RingHistory in production).
107
+ now = datetime.now(tz=UTC)
108
+ pipeline.backfill(generate_baseline(site_id=site.id, days=28, end=now))
109
+
110
+ # Live events.
111
+ for decision in pipeline.ingest_many(play_scenario(BUILTIN["lingering_stranger"], site_id=site.id, start=now)):
112
+ print(decision.action, decision.severity, decision.message)
113
+
114
+ # Periodic checks for what did not happen.
115
+ pipeline.sweep()
116
+ ```
117
+
118
+ ### Ring webhooks
119
+
120
+ ```python
121
+ from stoop.sources.ring import parse_ring_webhook, RingSignatureError
122
+
123
+ @app.post("/webhooks/ring")
124
+ async def ring_webhook(request):
125
+ body = await request.body()
126
+ try:
127
+ event = parse_ring_webhook(body, site_id=site_id_for(request), signing_key=RING_HMAC_KEY,
128
+ signature=request.headers.get("X-Signature"))
129
+ except RingSignatureError:
130
+ return Response(status_code=401)
131
+ pipeline.ingest(event)
132
+ return Response(status_code=200)
133
+ ```
134
+
135
+ ### Ring account linking
136
+
137
+ Ring links accounts two ways. The default, **one-way** flow starts in the Ring Appstore: Ring
138
+ posts an authorization code to your Token Exchange URL, then sends the user's browser to your
139
+ Account Link URL with a `nonce` and `time`. `RingLinker` handles both halves and the
140
+ **partner-initiated** PKCE flow, and keeps tokens fresh.
141
+
142
+ ```python
143
+ from stoop import Store
144
+ from stoop.sources.ring_oauth import RingLinker, RingOAuth, RingOAuthConfig, SqliteTokenStore
145
+
146
+ oauth = RingOAuth(RingOAuthConfig(client_id=..., client_secret=..., hmac_key=...))
147
+ linker = RingLinker(oauth, SqliteTokenStore(store, cipher=Fernet(key))) # any encrypt/decrypt pair
148
+
149
+ # Token Exchange URL (server to server): park the tokens, look up the Ring account id.
150
+ link_id = linker.receive_code(code)
151
+
152
+ # Account Link URL (browser, after your own sign-in): prove which parked token is this user's.
153
+ link_id = linker.claim_by_nonce(nonce=nonce, time_value=time, owner=user.id)
154
+
155
+ # Later: a valid access token, refreshed and persisted when needed.
156
+ with RingHistory(linker.access_token(link_id)) as ring: ...
157
+ ```
158
+
159
+ ### Ring history backfill
160
+
161
+ ```python
162
+ from stoop.sources.ring import RingHistory
163
+
164
+ with RingHistory(token) as ring: # Playground token or OAuth access token
165
+ for device in ring.devices():
166
+ pipeline.backfill(ring.events(site.id, device["id"], device_name=device["name"]))
167
+ ```
168
+
169
+ Documented Ring history carries no person/vehicle/package classification. Only webhooks do,
170
+ so backfilled motion is stored as `detected=unknown`.
171
+
172
+ ### Bedrock reasoning
173
+
174
+ ```python
175
+ from stoop import PolicyConfig, PolicyEngine
176
+ from stoop.reasoning.bedrock import BedrockReasoner
177
+
178
+ engine = PolicyEngine(store, config=PolicyConfig(refine_min_severity="medium"),
179
+ reasoner=BedrockReasoner(region_name="us-east-1"),
180
+ snapshot_fetcher=fetch_ring_snapshot) # optional: bytes for multimodal context
181
+ ```
182
+
183
+ ## Custom rules
184
+
185
+ Rules live in an ordered registry. The first rule that returns a decision wins, so specific
186
+ rules go before general ones. `home_rules()` returns a fresh copy of the default set; add,
187
+ move, replace or disable without forking:
188
+
189
+ ```python
190
+ from stoop import Action, Decision, EventKind, PolicyEngine, Severity, SiteKind
191
+ from stoop.policy import RuleContext, home_rules, home_sweeps
192
+
193
+ rules = home_rules()
194
+
195
+ @rules.add(before="unknown_visitor", kinds={EventKind.BUTTON_PRESS})
196
+ def lunch_courier(ctx: RuleContext) -> Decision | None:
197
+ """Weekday lunch doorbells at an office are couriers, not strangers."""
198
+ if 11 <= ctx.local.hour < 14 and ctx.site.kind is SiteKind.OFFICE:
199
+ return ctx.decide(Action.LOG, Severity.INFO, "lunch_courier", "Lunch delivery window.", f"Lunch delivery at {ctx.where}.")
200
+ return None
201
+
202
+ rules.disable("package_at_risk")
203
+
204
+ engine = PolicyEngine(store, rules=rules, sweeps=home_sweeps())
205
+ print(rules.names()) # evaluation order
206
+ ```
207
+
208
+ `RuleContext` gives a rule the event, site, current visit, matched expected visit, anomaly
209
+ score, quiet-hours flag, known people, and builders: `ctx.decide(...)`, `ctx.actions(...)`,
210
+ `ctx.recent_decision(...)`. Sweep checks work the same way through `SweepRegistry` and
211
+ `SweepContext`, and every default rule is a plain function in `stoop.policy.home_rules` you
212
+ can import and reuse.
213
+
214
+ ## Decisions
215
+
216
+ Every decision carries `action` (ignore, log, notify, escalate), `severity` (info, low,
217
+ medium, high), the `rule` that fired, a human-readable `message` and `reason`, an
218
+ `anomaly_score`, `suggested_actions` (some marked `sensitive`), and
219
+ `requires_confirmation`, which is true whenever any suggested action is sensitive.
220
+
221
+ Rules today: `expected_arrival`, `expected_entry`, `unknown_visitor`, `night_doorbell`,
222
+ `night_presence`, `night_door_open`, `lingering`, `package_delivered`, `package_at_risk`,
223
+ `unusual_time`, `sensor_alert`, `device_offline`, `no_show`, `inactivity`, `door_left_open`,
224
+ plus quiet `log`/`ignore` outcomes for routine motion.
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ uv sync --all-extras
230
+ uv run pytest
231
+ uv run ruff check src tests
232
+ ```
233
+
234
+ Integration tests use the community [`ring-sandbox`](https://github.com/josepha-mayo/ring-sandbox)
235
+ emulator in-process, so no Ring account is needed to run them.
236
+
237
+ ## Status
238
+
239
+ Version 0.1. The core pipeline, store, rules and reasoners run in production in two apps, but
240
+ the API may still change between minor versions. Every change is listed in
241
+ [`CHANGELOG.md`](CHANGELOG.md). Python 3.12 and 3.13 on Linux, macOS and Windows.
242
+
243
+ ## Contributing
244
+
245
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Security reports: [`SECURITY.md`](SECURITY.md).
246
+
247
+ ## Docs
248
+
249
+ - [`docs/roadmap.md`](docs/roadmap.md): what is reusable today, what is not yet, and what is
250
+ planned (OAuth token client, rule registry, FastAPI router, PyPI release).
251
+ - [`docs/friction-log.md`](docs/friction-log.md): platform obstacles met while building on
252
+ Ring, Alexa+ and AWS, and the workarounds.
253
+
254
+ ## License
255
+
256
+ MIT. See `LICENSE`.
stoop-0.1.0/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # Stoop
2
+
3
+ [![CI](https://github.com/iot-forge/Stoop/actions/workflows/ci.yml/badge.svg)](https://github.com/iot-forge/Stoop/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/stoop.svg)](https://pypi.org/project/stoop/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/stoop.svg)](https://pypi.org/project/stoop/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Turn front-door events into decisions a person can act on.
9
+
10
+ `stoop` is a small Python library that sits between a doorbell or camera feed (Ring today,
11
+ anything tomorrow) and the humans who care about what happens at that door. It remembers who
12
+ is expected, learns what is normal for that door, decides whether an event should be
13
+ ignored, logged, sent as a notification, or escalated, and keeps every sensitive action behind
14
+ a human confirmation.
15
+
16
+ It was built during the [Build, Ship, Shape: Amazon Developer Hackathon](https://amazonappdev2026.devpost.com/)
17
+ as the shared core of two products: a caregiving app on the Ring track and a conversational
18
+ property concierge on the Alexa+ track. The library is product-agnostic and MIT licensed.
19
+
20
+ ## What it does
21
+
22
+ ```
23
+ Ring webhook / history ──┐
24
+ Synthetic scenarios ─────┼─▶ Event ─▶ Store (SQLite) ─▶ PolicyEngine ─▶ Decision ─▶ Sinks
25
+ Your own source ─────────┘ ▲ ▲ │
26
+ │ └── RoutineModel ─┘ (what is normal here?)
27
+ └───── ExpectedVisit / Person (who is expected?)
28
+ ```
29
+
30
+ - **Events** (`stoop.events`): one normalized shape for motion, doorbell presses, door
31
+ sensors, device health and environmental sensors. Ring's `X-Signature` HMAC is verified
32
+ over the raw body.
33
+ - **Memory** (`stoop.memory`): sites, people and their roles, expected visits (one-off or
34
+ recurring), visits (events clustered into "someone came to the door"), decisions, and a
35
+ small key/value state. SQLite, standard library only.
36
+ - **Routines** (`stoop.policy.routines`): weekly rates per weekday and hour, an anomaly
37
+ score for "how surprising is this right now", and "does this door usually see someone
38
+ every day".
39
+ - **Policy** (`stoop.policy.engine`): deterministic rules that always produce a complete
40
+ decision, plus a sweep for things that did not happen: no-shows, inactivity, doors left
41
+ open. Notifications about the same visit are suppressed so one visitor is one alert.
42
+ - **Reasoning** (`stoop.reasoning`): an optional refinement step. The deterministic reasoner
43
+ adds context with no model call. The Bedrock reasoner uses the Converse API (Amazon Nova 2
44
+ Lite by default, multimodal when a snapshot is available). A reasoner can reword and move
45
+ severity by one step, and can never remove a confirmation requirement.
46
+ - **Pipeline** (`stoop.pipeline`): source → engine → sinks, with backfill for routine learning.
47
+
48
+ Reasoning runs on the ingestion path only. Anything that answers a question (a dashboard, an
49
+ MCP tool for Alexa+) reads precomputed state and stays fast.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install stoop # core
55
+ pip install "stoop[aws]" # + boto3 for the Bedrock reasoner
56
+ ```
57
+
58
+ Python 3.13 or newer.
59
+
60
+ ## Quick start
61
+
62
+ ```python
63
+ from datetime import UTC, datetime, time
64
+
65
+ from stoop import ExpectedVisit, Person, Pipeline, PolicyEngine, Role, Site, Store, LogSink
66
+ from stoop.sources.synthetic import BUILTIN, generate_baseline, play_scenario
67
+
68
+ store = Store("stoop.db")
69
+ site = store.put_site(Site(id="moms-house", name="Mom's house", timezone="America/New_York"))
70
+ maria = store.put_person(Person(site_id=site.id, name="Maria", role=Role.AIDE))
71
+ store.put_expected(ExpectedVisit(site_id=site.id, label="Morning aide", person_id=maria.id,
72
+ days_of_week=[0, 2, 4], local_start=time(9, 0), local_end=time(10, 30)))
73
+
74
+ pipeline = Pipeline(PolicyEngine(store), sinks=[LogSink()])
75
+
76
+ # Teach it what "normal" looks like (synthetic here; real history from RingHistory in production).
77
+ now = datetime.now(tz=UTC)
78
+ pipeline.backfill(generate_baseline(site_id=site.id, days=28, end=now))
79
+
80
+ # Live events.
81
+ for decision in pipeline.ingest_many(play_scenario(BUILTIN["lingering_stranger"], site_id=site.id, start=now)):
82
+ print(decision.action, decision.severity, decision.message)
83
+
84
+ # Periodic checks for what did not happen.
85
+ pipeline.sweep()
86
+ ```
87
+
88
+ ### Ring webhooks
89
+
90
+ ```python
91
+ from stoop.sources.ring import parse_ring_webhook, RingSignatureError
92
+
93
+ @app.post("/webhooks/ring")
94
+ async def ring_webhook(request):
95
+ body = await request.body()
96
+ try:
97
+ event = parse_ring_webhook(body, site_id=site_id_for(request), signing_key=RING_HMAC_KEY,
98
+ signature=request.headers.get("X-Signature"))
99
+ except RingSignatureError:
100
+ return Response(status_code=401)
101
+ pipeline.ingest(event)
102
+ return Response(status_code=200)
103
+ ```
104
+
105
+ ### Ring account linking
106
+
107
+ Ring links accounts two ways. The default, **one-way** flow starts in the Ring Appstore: Ring
108
+ posts an authorization code to your Token Exchange URL, then sends the user's browser to your
109
+ Account Link URL with a `nonce` and `time`. `RingLinker` handles both halves and the
110
+ **partner-initiated** PKCE flow, and keeps tokens fresh.
111
+
112
+ ```python
113
+ from stoop import Store
114
+ from stoop.sources.ring_oauth import RingLinker, RingOAuth, RingOAuthConfig, SqliteTokenStore
115
+
116
+ oauth = RingOAuth(RingOAuthConfig(client_id=..., client_secret=..., hmac_key=...))
117
+ linker = RingLinker(oauth, SqliteTokenStore(store, cipher=Fernet(key))) # any encrypt/decrypt pair
118
+
119
+ # Token Exchange URL (server to server): park the tokens, look up the Ring account id.
120
+ link_id = linker.receive_code(code)
121
+
122
+ # Account Link URL (browser, after your own sign-in): prove which parked token is this user's.
123
+ link_id = linker.claim_by_nonce(nonce=nonce, time_value=time, owner=user.id)
124
+
125
+ # Later: a valid access token, refreshed and persisted when needed.
126
+ with RingHistory(linker.access_token(link_id)) as ring: ...
127
+ ```
128
+
129
+ ### Ring history backfill
130
+
131
+ ```python
132
+ from stoop.sources.ring import RingHistory
133
+
134
+ with RingHistory(token) as ring: # Playground token or OAuth access token
135
+ for device in ring.devices():
136
+ pipeline.backfill(ring.events(site.id, device["id"], device_name=device["name"]))
137
+ ```
138
+
139
+ Documented Ring history carries no person/vehicle/package classification. Only webhooks do,
140
+ so backfilled motion is stored as `detected=unknown`.
141
+
142
+ ### Bedrock reasoning
143
+
144
+ ```python
145
+ from stoop import PolicyConfig, PolicyEngine
146
+ from stoop.reasoning.bedrock import BedrockReasoner
147
+
148
+ engine = PolicyEngine(store, config=PolicyConfig(refine_min_severity="medium"),
149
+ reasoner=BedrockReasoner(region_name="us-east-1"),
150
+ snapshot_fetcher=fetch_ring_snapshot) # optional: bytes for multimodal context
151
+ ```
152
+
153
+ ## Custom rules
154
+
155
+ Rules live in an ordered registry. The first rule that returns a decision wins, so specific
156
+ rules go before general ones. `home_rules()` returns a fresh copy of the default set; add,
157
+ move, replace or disable without forking:
158
+
159
+ ```python
160
+ from stoop import Action, Decision, EventKind, PolicyEngine, Severity, SiteKind
161
+ from stoop.policy import RuleContext, home_rules, home_sweeps
162
+
163
+ rules = home_rules()
164
+
165
+ @rules.add(before="unknown_visitor", kinds={EventKind.BUTTON_PRESS})
166
+ def lunch_courier(ctx: RuleContext) -> Decision | None:
167
+ """Weekday lunch doorbells at an office are couriers, not strangers."""
168
+ if 11 <= ctx.local.hour < 14 and ctx.site.kind is SiteKind.OFFICE:
169
+ return ctx.decide(Action.LOG, Severity.INFO, "lunch_courier", "Lunch delivery window.", f"Lunch delivery at {ctx.where}.")
170
+ return None
171
+
172
+ rules.disable("package_at_risk")
173
+
174
+ engine = PolicyEngine(store, rules=rules, sweeps=home_sweeps())
175
+ print(rules.names()) # evaluation order
176
+ ```
177
+
178
+ `RuleContext` gives a rule the event, site, current visit, matched expected visit, anomaly
179
+ score, quiet-hours flag, known people, and builders: `ctx.decide(...)`, `ctx.actions(...)`,
180
+ `ctx.recent_decision(...)`. Sweep checks work the same way through `SweepRegistry` and
181
+ `SweepContext`, and every default rule is a plain function in `stoop.policy.home_rules` you
182
+ can import and reuse.
183
+
184
+ ## Decisions
185
+
186
+ Every decision carries `action` (ignore, log, notify, escalate), `severity` (info, low,
187
+ medium, high), the `rule` that fired, a human-readable `message` and `reason`, an
188
+ `anomaly_score`, `suggested_actions` (some marked `sensitive`), and
189
+ `requires_confirmation`, which is true whenever any suggested action is sensitive.
190
+
191
+ Rules today: `expected_arrival`, `expected_entry`, `unknown_visitor`, `night_doorbell`,
192
+ `night_presence`, `night_door_open`, `lingering`, `package_delivered`, `package_at_risk`,
193
+ `unusual_time`, `sensor_alert`, `device_offline`, `no_show`, `inactivity`, `door_left_open`,
194
+ plus quiet `log`/`ignore` outcomes for routine motion.
195
+
196
+ ## Development
197
+
198
+ ```bash
199
+ uv sync --all-extras
200
+ uv run pytest
201
+ uv run ruff check src tests
202
+ ```
203
+
204
+ Integration tests use the community [`ring-sandbox`](https://github.com/josepha-mayo/ring-sandbox)
205
+ emulator in-process, so no Ring account is needed to run them.
206
+
207
+ ## Status
208
+
209
+ Version 0.1. The core pipeline, store, rules and reasoners run in production in two apps, but
210
+ the API may still change between minor versions. Every change is listed in
211
+ [`CHANGELOG.md`](CHANGELOG.md). Python 3.12 and 3.13 on Linux, macOS and Windows.
212
+
213
+ ## Contributing
214
+
215
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Security reports: [`SECURITY.md`](SECURITY.md).
216
+
217
+ ## Docs
218
+
219
+ - [`docs/roadmap.md`](docs/roadmap.md): what is reusable today, what is not yet, and what is
220
+ planned (OAuth token client, rule registry, FastAPI router, PyPI release).
221
+ - [`docs/friction-log.md`](docs/friction-log.md): platform obstacles met while building on
222
+ Ring, Alexa+ and AWS, and the workarounds.
223
+
224
+ ## License
225
+
226
+ MIT. See `LICENSE`.
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "stoop"
3
+ version = "0.1.0"
4
+ description = "Turn front-door events (Ring and others) into decisions a person can act on: memory, routines, policy, and human-gated actions."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = [
10
+ "ring",
11
+ "doorbell",
12
+ "caregiving",
13
+ "agent",
14
+ "mcp",
15
+ "bedrock",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Home Automation",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "httpx>=0.27",
30
+ "pydantic>=2",
31
+ "tzdata>=2025.1; sys_platform == 'win32'",
32
+ ]
33
+
34
+ [[project.authors]]
35
+ name = "Uladzislau Bayouski"
36
+ email = "uladzislaubayouski@gmail.com"
37
+
38
+ [project.optional-dependencies]
39
+ aws = ["boto3>=1.40"]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/iot-forge/Stoop"
43
+ Repository = "https://github.com/iot-forge/Stoop"
44
+ Issues = "https://github.com/iot-forge/Stoop/issues"
45
+ Changelog = "https://github.com/iot-forge/Stoop/blob/main/CHANGELOG.md"
46
+
47
+ [build-system]
48
+ requires = ["uv_build>=0.12.19,<0.13.0"]
49
+ build-backend = "uv_build"
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8",
54
+ "pytest-asyncio>=1.4.0",
55
+ "ring-sandbox[server]>=0.2.0",
56
+ "ruff>=0.16.9",
57
+ ]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+
62
+ [tool.ruff]
63
+ line-length = 140
64
+ target-version = "py313"
65
+
66
+ [tool.ruff.lint]
67
+ select = [
68
+ "E",
69
+ "F",
70
+ "I",
71
+ "B",
72
+ "UP",
73
+ "SIM",
74
+ ]
75
+ ignore = ["E501"]
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "stoop"
3
+ version = "0.1.0"
4
+ description = "Turn front-door events (Ring and others) into decisions a person can act on: memory, routines, policy, and human-gated actions."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Uladzislau Bayouski", email = "uladzislaubayouski@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ keywords = ["ring", "doorbell", "caregiving", "agent", "mcp", "bedrock"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Home Automation",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27",
26
+ "pydantic>=2",
27
+ "tzdata>=2025.1; sys_platform == 'win32'",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ aws = [
32
+ "boto3>=1.40",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/iot-forge/Stoop"
37
+ Repository = "https://github.com/iot-forge/Stoop"
38
+ Issues = "https://github.com/iot-forge/Stoop/issues"
39
+ Changelog = "https://github.com/iot-forge/Stoop/blob/main/CHANGELOG.md"
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.12.19,<0.13.0"]
43
+ build-backend = "uv_build"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest>=8",
48
+ "pytest-asyncio>=1.4.0",
49
+ "ring-sandbox[server]>=0.2.0",
50
+ "ruff>=0.16.9",
51
+ ]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+
56
+ [tool.ruff]
57
+ line-length = 140
58
+ target-version = "py313"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "B", "UP", "SIM"]
62
+ ignore = ["E501"]