optout 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.
- optout-0.1.0/PKG-INFO +227 -0
- optout-0.1.0/README.md +188 -0
- optout-0.1.0/pyproject.toml +93 -0
- optout-0.1.0/setup.cfg +4 -0
- optout-0.1.0/src/optout/__init__.py +3 -0
- optout-0.1.0/src/optout/brokers/__init__.py +0 -0
- optout-0.1.0/src/optout/brokers/loader.py +36 -0
- optout-0.1.0/src/optout/brokers/registry.py +56 -0
- optout-0.1.0/src/optout/brokers/schema.py +196 -0
- optout-0.1.0/src/optout/cli.py +1185 -0
- optout-0.1.0/src/optout/config.py +137 -0
- optout-0.1.0/src/optout/data/__init__.py +0 -0
- optout-0.1.0/src/optout/data/brokers/__init__.py +0 -0
- optout-0.1.0/src/optout/data/brokers/beenverified.yml +197 -0
- optout-0.1.0/src/optout/data/brokers/mylife.yml +122 -0
- optout-0.1.0/src/optout/data/brokers/radaris.yml +127 -0
- optout-0.1.0/src/optout/data/brokers/spokeo.yml +99 -0
- optout-0.1.0/src/optout/data/brokers/whitepages.yml +149 -0
- optout-0.1.0/src/optout/db.py +265 -0
- optout-0.1.0/src/optout/engine/__init__.py +24 -0
- optout-0.1.0/src/optout/engine/browser.py +45 -0
- optout-0.1.0/src/optout/engine/dispatcher.py +73 -0
- optout-0.1.0/src/optout/engine/escalation.py +1 -0
- optout-0.1.0/src/optout/engine/methods/__init__.py +0 -0
- optout-0.1.0/src/optout/engine/methods/email.py +158 -0
- optout-0.1.0/src/optout/engine/methods/manual.py +16 -0
- optout-0.1.0/src/optout/engine/methods/postal.py +18 -0
- optout-0.1.0/src/optout/engine/methods/web_form.py +728 -0
- optout-0.1.0/src/optout/logging.py +85 -0
- optout-0.1.0/src/optout/monitoring.py +172 -0
- optout-0.1.0/src/optout/scan/__init__.py +0 -0
- optout-0.1.0/src/optout/scan/scanner.py +111 -0
- optout-0.1.0/src/optout/utils/__init__.py +0 -0
- optout-0.1.0/src/optout/utils/playwright_helpers.py +42 -0
- optout-0.1.0/src/optout/utils/statutes.py +124 -0
- optout-0.1.0/src/optout/verify.py +146 -0
- optout-0.1.0/src/optout/wizard.py +365 -0
- optout-0.1.0/src/optout.egg-info/PKG-INFO +227 -0
- optout-0.1.0/src/optout.egg-info/SOURCES.txt +53 -0
- optout-0.1.0/src/optout.egg-info/dependency_links.txt +1 -0
- optout-0.1.0/src/optout.egg-info/entry_points.txt +2 -0
- optout-0.1.0/src/optout.egg-info/requires.txt +18 -0
- optout-0.1.0/src/optout.egg-info/top_level.txt +1 -0
- optout-0.1.0/tests/test_broker_loader.py +105 -0
- optout-0.1.0/tests/test_brokers_commands.py +137 -0
- optout-0.1.0/tests/test_db.py +213 -0
- optout-0.1.0/tests/test_dispatcher.py +405 -0
- optout-0.1.0/tests/test_doctor.py +188 -0
- optout-0.1.0/tests/test_engine_email.py +335 -0
- optout-0.1.0/tests/test_engine_web_form.py +534 -0
- optout-0.1.0/tests/test_production_brokers.py +146 -0
- optout-0.1.0/tests/test_scan_monitor.py +458 -0
- optout-0.1.0/tests/test_status_command.py +347 -0
- optout-0.1.0/tests/test_verify.py +217 -0
- optout-0.1.0/tests/test_wizard.py +137 -0
optout-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optout
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-hosted CLI that automates CCPA/CPRA data-broker opt-out requests via Playwright
|
|
5
|
+
Author: Blake Matas
|
|
6
|
+
License-Expression: AGPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/Blake104/OptOut
|
|
8
|
+
Project-URL: Repository, https://github.com/Blake104/OptOut
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Blake104/OptOut/issues
|
|
10
|
+
Keywords: privacy,ccpa,cpra,data-broker,opt-out,playwright,automation
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: <3.14,>=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: typer[all]>=0.12
|
|
23
|
+
Requires-Dist: rich>=13
|
|
24
|
+
Requires-Dist: pydantic>=2
|
|
25
|
+
Requires-Dist: pydantic-settings>=2
|
|
26
|
+
Requires-Dist: sqlmodel>=0.0.19
|
|
27
|
+
Requires-Dist: pyyaml>=6
|
|
28
|
+
Requires-Dist: jinja2>=3
|
|
29
|
+
Requires-Dist: httpx>=0.27
|
|
30
|
+
Requires-Dist: playwright>=1.44
|
|
31
|
+
Requires-Dist: structlog>=23.0.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
37
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
38
|
+
Requires-Dist: aiosmtpd>=1.4; extra == "dev"
|
|
39
|
+
|
|
40
|
+
# OptOut
|
|
41
|
+
|
|
42
|
+
Self-hosted CLI tool that automates submitting opt-out and data-deletion requests to data brokers.
|
|
43
|
+
|
|
44
|
+
You run it on your own machine with your own information. There is no central server, no accounts, no SaaS. Because *you* are the data subject submitting on your own behalf, the entire "authorized agent" legal apparatus that commercial services like DeleteMe and Optery have to build does not apply here.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What it does and does not do
|
|
49
|
+
|
|
50
|
+
**Does:**
|
|
51
|
+
- Submit opt-out / deletion requests to people-search and marketing data brokers.
|
|
52
|
+
- Track deadlines (CCPA: 45 days, GDPR: 30 days, etc.) and surface overdue requests.
|
|
53
|
+
- Re-scan brokers periodically and re-submit if your data reappears.
|
|
54
|
+
- Open a real browser window (Chromium) so *you* solve CAPTCHAs — no bot-detection arms race.
|
|
55
|
+
- Send opt-out emails from your own inbox so brokers can't block "the service."
|
|
56
|
+
|
|
57
|
+
**Does not:**
|
|
58
|
+
- Submit requests for anyone other than the person running the tool.
|
|
59
|
+
- Touch credit bureaus (Experian, Equifax, TransUnion) — those have separate, regulated dispute processes.
|
|
60
|
+
- Remove government records, court filings, or news articles.
|
|
61
|
+
- Guarantee removal from sites that ignore opt-out requests.
|
|
62
|
+
- Store your personal information anywhere except your own machine (`~/.config/optout/`).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
**Requirements:** Python 3.11+ and `pip` (or `uv`).
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/your-username/optout.git
|
|
72
|
+
cd optout
|
|
73
|
+
pip install -e ".[dev]" # or: uv pip install -e ".[dev]"
|
|
74
|
+
playwright install chromium # one-time browser download
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quickstart
|
|
80
|
+
|
|
81
|
+
### 1. Initialize your profile
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
optout init
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The interactive wizard asks for your name, address, email, and SMTP credentials. It writes `~/.config/optout/config.yml` (mode `600`) and creates the local SQLite database.
|
|
88
|
+
|
|
89
|
+
### 2. Queue brokers for opt-out
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
optout brokers list # see all supported brokers
|
|
93
|
+
optout queue # queue all brokers
|
|
94
|
+
optout queue --broker whitepages # queue one specific broker
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Submit opt-out requests
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
optout submit
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A browser window opens for each web-form broker. Follow the on-screen prompts — select your listing, solve CAPTCHAs, confirm emails. Email-method brokers are submitted automatically using your configured SMTP credentials.
|
|
104
|
+
|
|
105
|
+
### 4. Check status
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
optout status
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Shows a table of every submission: method used, current status, statutory deadline, and days remaining.
|
|
112
|
+
|
|
113
|
+
### 5. Re-scan periodically
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
optout monitor
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Checks which brokers still list you and re-queues any that have re-added your data. Cron-friendly — run it monthly.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
optout init # interactive setup wizard
|
|
127
|
+
optout brokers list [--category] # list all known brokers
|
|
128
|
+
optout brokers info <slug> # details + your submission history for one broker
|
|
129
|
+
optout scan [--broker SLUG] # check which brokers currently list you
|
|
130
|
+
optout queue [--broker SLUG] # add brokers to the submission queue
|
|
131
|
+
optout submit [--broker SLUG] # process the queue and submit opt-outs
|
|
132
|
+
optout status [--broker] [--status] # table of submissions, deadlines, callouts
|
|
133
|
+
optout monitor # one-shot re-scan (schedule with cron or launchd)
|
|
134
|
+
optout escalate # send follow-ups for submissions past their deadline
|
|
135
|
+
optout export # dump all submission history as JSON
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
`~/.config/optout/config.yml` is generated by `optout init`. Key sections:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
profile:
|
|
146
|
+
legal_name: "Jane Q Public"
|
|
147
|
+
current_address:
|
|
148
|
+
street: "123 Main St"
|
|
149
|
+
city: "Austin"
|
|
150
|
+
state: "TX"
|
|
151
|
+
zip: "78701"
|
|
152
|
+
emails:
|
|
153
|
+
current: ["jane@example.com"]
|
|
154
|
+
phones:
|
|
155
|
+
current: ["+15125551234"]
|
|
156
|
+
|
|
157
|
+
email:
|
|
158
|
+
method: smtp
|
|
159
|
+
smtp:
|
|
160
|
+
host: smtp.gmail.com
|
|
161
|
+
port: 587
|
|
162
|
+
username: jane@example.com
|
|
163
|
+
password_env: OPTOUT_SMTP_PASSWORD # never stored in the file itself
|
|
164
|
+
|
|
165
|
+
playwright:
|
|
166
|
+
headless: false # keep false so you can solve CAPTCHAs
|
|
167
|
+
slow_mo_ms: 0
|
|
168
|
+
|
|
169
|
+
monitoring:
|
|
170
|
+
rescan_interval_days: 30
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Sensitive values (SMTP passwords) are read from environment variables, not stored in the config file.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Supported brokers
|
|
178
|
+
|
|
179
|
+
| Slug | Name | Method | Verification |
|
|
180
|
+
|---|---|---|---|
|
|
181
|
+
| `whitepages` | Whitepages | Web form | Phone (SMS) |
|
|
182
|
+
| `spokeo` | Spokeo | Web form | Email |
|
|
183
|
+
| `beenverified` | BeenVerified | Web form | Email |
|
|
184
|
+
| `radaris` | Radaris | Email | None |
|
|
185
|
+
| `mylife` | MyLife | Email | None |
|
|
186
|
+
|
|
187
|
+
Community contributions add brokers as YAML files — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Running tests
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pytest # full suite
|
|
195
|
+
pytest tests/test_production_brokers.py # validate all broker YAMLs only
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Legal basis
|
|
201
|
+
|
|
202
|
+
OptOut cites the applicable statute in every opt-out email and web-form submission:
|
|
203
|
+
|
|
204
|
+
- **CCPA §1798.105** (California Consumer Privacy Act) — right to deletion, 45-day response window
|
|
205
|
+
- **CPRA** (California Privacy Rights Act) — extends CCPA, same 45-day window
|
|
206
|
+
- **GDPR Art. 17** (General Data Protection Regulation) — right to erasure, 30-day response window
|
|
207
|
+
- **VCDPA, CPA, CTDPA, UCPA** — state-level equivalents
|
|
208
|
+
|
|
209
|
+
Each broker's YAML lists which statutes apply. The tool automatically includes the correct citation in every submission.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Honest limitations
|
|
214
|
+
|
|
215
|
+
- Removal is not guaranteed. Some brokers comply reliably; others are slow or re-add data.
|
|
216
|
+
- Web-form flows break when brokers redesign their opt-out pages. If a broker's steps stop working, open an issue or submit a YAML fix.
|
|
217
|
+
- CAPTCHAs require your attention — the tool will pause and prompt you.
|
|
218
|
+
- Email submissions depend on your SMTP server being reachable and not blocked by the broker.
|
|
219
|
+
- Data frequently reappears (60–90 days is typical). Use `optout monitor` to catch this.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
AGPL-3.0-only. See [LICENSE](LICENSE).
|
|
226
|
+
|
|
227
|
+
This license was chosen intentionally: anyone who forks this and runs it as a hosted service must publish their changes. The project's legal model depends on each user running their own copy — a hosted fork changes the legal character of the tool.
|
optout-0.1.0/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# OptOut
|
|
2
|
+
|
|
3
|
+
Self-hosted CLI tool that automates submitting opt-out and data-deletion requests to data brokers.
|
|
4
|
+
|
|
5
|
+
You run it on your own machine with your own information. There is no central server, no accounts, no SaaS. Because *you* are the data subject submitting on your own behalf, the entire "authorized agent" legal apparatus that commercial services like DeleteMe and Optery have to build does not apply here.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What it does and does not do
|
|
10
|
+
|
|
11
|
+
**Does:**
|
|
12
|
+
- Submit opt-out / deletion requests to people-search and marketing data brokers.
|
|
13
|
+
- Track deadlines (CCPA: 45 days, GDPR: 30 days, etc.) and surface overdue requests.
|
|
14
|
+
- Re-scan brokers periodically and re-submit if your data reappears.
|
|
15
|
+
- Open a real browser window (Chromium) so *you* solve CAPTCHAs — no bot-detection arms race.
|
|
16
|
+
- Send opt-out emails from your own inbox so brokers can't block "the service."
|
|
17
|
+
|
|
18
|
+
**Does not:**
|
|
19
|
+
- Submit requests for anyone other than the person running the tool.
|
|
20
|
+
- Touch credit bureaus (Experian, Equifax, TransUnion) — those have separate, regulated dispute processes.
|
|
21
|
+
- Remove government records, court filings, or news articles.
|
|
22
|
+
- Guarantee removal from sites that ignore opt-out requests.
|
|
23
|
+
- Store your personal information anywhere except your own machine (`~/.config/optout/`).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
**Requirements:** Python 3.11+ and `pip` (or `uv`).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/your-username/optout.git
|
|
33
|
+
cd optout
|
|
34
|
+
pip install -e ".[dev]" # or: uv pip install -e ".[dev]"
|
|
35
|
+
playwright install chromium # one-time browser download
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
### 1. Initialize your profile
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
optout init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The interactive wizard asks for your name, address, email, and SMTP credentials. It writes `~/.config/optout/config.yml` (mode `600`) and creates the local SQLite database.
|
|
49
|
+
|
|
50
|
+
### 2. Queue brokers for opt-out
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
optout brokers list # see all supported brokers
|
|
54
|
+
optout queue # queue all brokers
|
|
55
|
+
optout queue --broker whitepages # queue one specific broker
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Submit opt-out requests
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
optout submit
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A browser window opens for each web-form broker. Follow the on-screen prompts — select your listing, solve CAPTCHAs, confirm emails. Email-method brokers are submitted automatically using your configured SMTP credentials.
|
|
65
|
+
|
|
66
|
+
### 4. Check status
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
optout status
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Shows a table of every submission: method used, current status, statutory deadline, and days remaining.
|
|
73
|
+
|
|
74
|
+
### 5. Re-scan periodically
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
optout monitor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Checks which brokers still list you and re-queues any that have re-added your data. Cron-friendly — run it monthly.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
optout init # interactive setup wizard
|
|
88
|
+
optout brokers list [--category] # list all known brokers
|
|
89
|
+
optout brokers info <slug> # details + your submission history for one broker
|
|
90
|
+
optout scan [--broker SLUG] # check which brokers currently list you
|
|
91
|
+
optout queue [--broker SLUG] # add brokers to the submission queue
|
|
92
|
+
optout submit [--broker SLUG] # process the queue and submit opt-outs
|
|
93
|
+
optout status [--broker] [--status] # table of submissions, deadlines, callouts
|
|
94
|
+
optout monitor # one-shot re-scan (schedule with cron or launchd)
|
|
95
|
+
optout escalate # send follow-ups for submissions past their deadline
|
|
96
|
+
optout export # dump all submission history as JSON
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
`~/.config/optout/config.yml` is generated by `optout init`. Key sections:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
profile:
|
|
107
|
+
legal_name: "Jane Q Public"
|
|
108
|
+
current_address:
|
|
109
|
+
street: "123 Main St"
|
|
110
|
+
city: "Austin"
|
|
111
|
+
state: "TX"
|
|
112
|
+
zip: "78701"
|
|
113
|
+
emails:
|
|
114
|
+
current: ["jane@example.com"]
|
|
115
|
+
phones:
|
|
116
|
+
current: ["+15125551234"]
|
|
117
|
+
|
|
118
|
+
email:
|
|
119
|
+
method: smtp
|
|
120
|
+
smtp:
|
|
121
|
+
host: smtp.gmail.com
|
|
122
|
+
port: 587
|
|
123
|
+
username: jane@example.com
|
|
124
|
+
password_env: OPTOUT_SMTP_PASSWORD # never stored in the file itself
|
|
125
|
+
|
|
126
|
+
playwright:
|
|
127
|
+
headless: false # keep false so you can solve CAPTCHAs
|
|
128
|
+
slow_mo_ms: 0
|
|
129
|
+
|
|
130
|
+
monitoring:
|
|
131
|
+
rescan_interval_days: 30
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Sensitive values (SMTP passwords) are read from environment variables, not stored in the config file.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Supported brokers
|
|
139
|
+
|
|
140
|
+
| Slug | Name | Method | Verification |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `whitepages` | Whitepages | Web form | Phone (SMS) |
|
|
143
|
+
| `spokeo` | Spokeo | Web form | Email |
|
|
144
|
+
| `beenverified` | BeenVerified | Web form | Email |
|
|
145
|
+
| `radaris` | Radaris | Email | None |
|
|
146
|
+
| `mylife` | MyLife | Email | None |
|
|
147
|
+
|
|
148
|
+
Community contributions add brokers as YAML files — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Running tests
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pytest # full suite
|
|
156
|
+
pytest tests/test_production_brokers.py # validate all broker YAMLs only
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Legal basis
|
|
162
|
+
|
|
163
|
+
OptOut cites the applicable statute in every opt-out email and web-form submission:
|
|
164
|
+
|
|
165
|
+
- **CCPA §1798.105** (California Consumer Privacy Act) — right to deletion, 45-day response window
|
|
166
|
+
- **CPRA** (California Privacy Rights Act) — extends CCPA, same 45-day window
|
|
167
|
+
- **GDPR Art. 17** (General Data Protection Regulation) — right to erasure, 30-day response window
|
|
168
|
+
- **VCDPA, CPA, CTDPA, UCPA** — state-level equivalents
|
|
169
|
+
|
|
170
|
+
Each broker's YAML lists which statutes apply. The tool automatically includes the correct citation in every submission.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Honest limitations
|
|
175
|
+
|
|
176
|
+
- Removal is not guaranteed. Some brokers comply reliably; others are slow or re-add data.
|
|
177
|
+
- Web-form flows break when brokers redesign their opt-out pages. If a broker's steps stop working, open an issue or submit a YAML fix.
|
|
178
|
+
- CAPTCHAs require your attention — the tool will pause and prompt you.
|
|
179
|
+
- Email submissions depend on your SMTP server being reachable and not blocked by the broker.
|
|
180
|
+
- Data frequently reappears (60–90 days is typical). Use `optout monitor` to catch this.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
AGPL-3.0-only. See [LICENSE](LICENSE).
|
|
187
|
+
|
|
188
|
+
This license was chosen intentionally: anyone who forks this and runs it as a hosted service must publish their changes. The project's legal model depends on each user running their own copy — a hosted fork changes the legal character of the tool.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "optout"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Self-hosted CLI that automates CCPA/CPRA data-broker opt-out requests via Playwright"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "AGPL-3.0-only"
|
|
11
|
+
requires-python = ">=3.12,<3.14"
|
|
12
|
+
authors = [{ name = "Blake Matas" }]
|
|
13
|
+
keywords = ["privacy", "ccpa", "cpra", "data-broker", "opt-out", "playwright", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typer[all]>=0.12",
|
|
27
|
+
"rich>=13",
|
|
28
|
+
"pydantic>=2",
|
|
29
|
+
"pydantic-settings>=2",
|
|
30
|
+
"sqlmodel>=0.0.19",
|
|
31
|
+
"pyyaml>=6",
|
|
32
|
+
"jinja2>=3",
|
|
33
|
+
"httpx>=0.27",
|
|
34
|
+
"playwright>=1.44",
|
|
35
|
+
"structlog>=23.0.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/Blake104/OptOut"
|
|
40
|
+
Repository = "https://github.com/Blake104/OptOut"
|
|
41
|
+
"Bug Tracker" = "https://github.com/Blake104/OptOut/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
optout = "optout.cli:app"
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=8",
|
|
49
|
+
"pytest-asyncio>=0.23",
|
|
50
|
+
"ruff>=0.4",
|
|
51
|
+
"mypy>=1.10",
|
|
52
|
+
"types-PyYAML",
|
|
53
|
+
"aiosmtpd>=1.4",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.package-data]
|
|
60
|
+
optout = ["data/brokers/*.yml"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py312"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I", "UP"]
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
python_version = "3.12"
|
|
71
|
+
ignore_missing_imports = true
|
|
72
|
+
warn_unused_ignores = true
|
|
73
|
+
warn_return_any = false
|
|
74
|
+
disallow_untyped_defs = false
|
|
75
|
+
check_untyped_defs = true
|
|
76
|
+
|
|
77
|
+
# Playwright, SQLModel, and the engine use heavy Any-based dynamic dispatch.
|
|
78
|
+
# Type-check the schema, config, and CLI where types are well-defined.
|
|
79
|
+
[[tool.mypy.overrides]]
|
|
80
|
+
module = [
|
|
81
|
+
"optout.engine.*",
|
|
82
|
+
"optout.scan.*",
|
|
83
|
+
"optout.monitoring",
|
|
84
|
+
"optout.db",
|
|
85
|
+
"optout.cli",
|
|
86
|
+
"optout.wizard",
|
|
87
|
+
]
|
|
88
|
+
ignore_errors = true
|
|
89
|
+
|
|
90
|
+
[tool.pytest.ini_options]
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
asyncio_mode = "auto"
|
|
93
|
+
asyncio_default_fixture_loop_scope = "function"
|
optout-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Loads and validates broker YAML files against BrokerDef."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from .schema import BrokerDef
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_broker(path: Path) -> BrokerDef:
|
|
14
|
+
"""Parse and validate a single broker YAML file."""
|
|
15
|
+
data = yaml.safe_load(path.read_text())
|
|
16
|
+
try:
|
|
17
|
+
return BrokerDef.model_validate(data)
|
|
18
|
+
except ValidationError as exc:
|
|
19
|
+
raise ValueError(f"Invalid broker definition at {path}:\n{exc}") from exc
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_all_brokers(brokers_dir: Path) -> list[BrokerDef]:
|
|
23
|
+
"""Load every *.yml file in brokers_dir, sorted by slug."""
|
|
24
|
+
results: list[BrokerDef] = []
|
|
25
|
+
errors: list[str] = []
|
|
26
|
+
|
|
27
|
+
for yml in sorted(brokers_dir.glob("*.yml")):
|
|
28
|
+
try:
|
|
29
|
+
results.append(load_broker(yml))
|
|
30
|
+
except (ValueError, yaml.YAMLError) as exc:
|
|
31
|
+
errors.append(str(exc))
|
|
32
|
+
|
|
33
|
+
if errors:
|
|
34
|
+
raise ValueError(f"{len(errors)} broker(s) failed validation:\n" + "\n\n".join(errors))
|
|
35
|
+
|
|
36
|
+
return results
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""In-memory broker catalog, populated from the brokers/ YAML directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .loader import load_all_brokers
|
|
10
|
+
from .schema import BrokerDef
|
|
11
|
+
|
|
12
|
+
_registry: dict[str, BrokerDef] = {}
|
|
13
|
+
_loaded = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _brokers_dir() -> Path:
|
|
17
|
+
"""Return the directory containing broker YAML files.
|
|
18
|
+
|
|
19
|
+
Priority:
|
|
20
|
+
1. OPTOUT_BROKERS_DIR env var — lets users/tests override with a custom set.
|
|
21
|
+
2. importlib.resources — finds the YAMLs bundled inside the installed package,
|
|
22
|
+
works for both editable installs (real filesystem path) and wheel installs.
|
|
23
|
+
"""
|
|
24
|
+
env = os.environ.get("OPTOUT_BROKERS_DIR")
|
|
25
|
+
if env:
|
|
26
|
+
return Path(env)
|
|
27
|
+
return Path(str(files("optout").joinpath("data/brokers")))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_registry(brokers_dir: Path | None = None) -> None:
|
|
31
|
+
global _registry, _loaded
|
|
32
|
+
directory = brokers_dir or _brokers_dir()
|
|
33
|
+
if not directory.exists():
|
|
34
|
+
_registry = {}
|
|
35
|
+
_loaded = True
|
|
36
|
+
return
|
|
37
|
+
_registry = {b.slug: b for b in load_all_brokers(directory)}
|
|
38
|
+
_loaded = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ensure_loaded() -> None:
|
|
42
|
+
if not _loaded:
|
|
43
|
+
load_registry()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_broker(slug: str) -> BrokerDef:
|
|
47
|
+
_ensure_loaded()
|
|
48
|
+
try:
|
|
49
|
+
return _registry[slug]
|
|
50
|
+
except KeyError:
|
|
51
|
+
raise KeyError(f"Unknown broker slug: '{slug}'. Run `optout brokers list` to see options.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def all_brokers() -> list[BrokerDef]:
|
|
55
|
+
_ensure_loaded()
|
|
56
|
+
return sorted(_registry.values(), key=lambda b: b.name)
|