android-watcher 1.0.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.
- android_watcher-1.0.0/.editorconfig +24 -0
- android_watcher-1.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +45 -0
- android_watcher-1.0.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- android_watcher-1.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +24 -0
- android_watcher-1.0.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- android_watcher-1.0.0/.github/workflows/ci.yml +50 -0
- android_watcher-1.0.0/.github/workflows/release.yml +112 -0
- android_watcher-1.0.0/.github/workflows/seed.yml +67 -0
- android_watcher-1.0.0/.gitignore +49 -0
- android_watcher-1.0.0/.pre-commit-config.yaml +33 -0
- android_watcher-1.0.0/CLAUDE.md +158 -0
- android_watcher-1.0.0/CODE_OF_CONDUCT.md +41 -0
- android_watcher-1.0.0/CONTRIBUTING.md +106 -0
- android_watcher-1.0.0/Formula/android-watcher.rb +27 -0
- android_watcher-1.0.0/LICENSE +21 -0
- android_watcher-1.0.0/PKG-INFO +310 -0
- android_watcher-1.0.0/README.md +284 -0
- android_watcher-1.0.0/SECURITY.md +36 -0
- android_watcher-1.0.0/assets/logo.svg +19 -0
- android_watcher-1.0.0/docs/scheduling.md +157 -0
- android_watcher-1.0.0/pyproject.toml +80 -0
- android_watcher-1.0.0/scripts/build_seed.py +126 -0
- android_watcher-1.0.0/scripts/verify_catalog.py +90 -0
- android_watcher-1.0.0/src/android_watcher/__init__.py +10 -0
- android_watcher-1.0.0/src/android_watcher/catalog/__init__.py +32 -0
- android_watcher-1.0.0/src/android_watcher/catalog/catalog.toml +531 -0
- android_watcher-1.0.0/src/android_watcher/cli.py +161 -0
- android_watcher-1.0.0/src/android_watcher/config.py +262 -0
- android_watcher-1.0.0/src/android_watcher/detect/__init__.py +1 -0
- android_watcher-1.0.0/src/android_watcher/detect/_normalize.py +192 -0
- android_watcher-1.0.0/src/android_watcher/detect/android_sitemap.py +540 -0
- android_watcher-1.0.0/src/android_watcher/detect/base.py +14 -0
- android_watcher-1.0.0/src/android_watcher/detect/content.py +99 -0
- android_watcher-1.0.0/src/android_watcher/detect/feed.py +135 -0
- android_watcher-1.0.0/src/android_watcher/detect/sitemap.py +203 -0
- android_watcher-1.0.0/src/android_watcher/doctor.py +125 -0
- android_watcher-1.0.0/src/android_watcher/fetch.py +162 -0
- android_watcher-1.0.0/src/android_watcher/group.py +79 -0
- android_watcher-1.0.0/src/android_watcher/lock.py +32 -0
- android_watcher-1.0.0/src/android_watcher/models.py +156 -0
- android_watcher-1.0.0/src/android_watcher/notify/__init__.py +1 -0
- android_watcher-1.0.0/src/android_watcher/notify/base.py +21 -0
- android_watcher-1.0.0/src/android_watcher/notify/email.py +52 -0
- android_watcher-1.0.0/src/android_watcher/notify/html.py +114 -0
- android_watcher-1.0.0/src/android_watcher/notify/render.py +239 -0
- android_watcher-1.0.0/src/android_watcher/notify/slack.py +124 -0
- android_watcher-1.0.0/src/android_watcher/notify/telegram.py +46 -0
- android_watcher-1.0.0/src/android_watcher/rank.py +84 -0
- android_watcher-1.0.0/src/android_watcher/registry.py +38 -0
- android_watcher-1.0.0/src/android_watcher/run.py +283 -0
- android_watcher-1.0.0/src/android_watcher/schedule.py +488 -0
- android_watcher-1.0.0/src/android_watcher/seed/__init__.py +45 -0
- android_watcher-1.0.0/src/android_watcher/seed/seed.sql.gz +0 -0
- android_watcher-1.0.0/src/android_watcher/store.py +492 -0
- android_watcher-1.0.0/src/android_watcher/triage/__init__.py +1 -0
- android_watcher-1.0.0/src/android_watcher/triage/base.py +25 -0
- android_watcher-1.0.0/src/android_watcher/triage/claude_cli.py +185 -0
- android_watcher-1.0.0/src/android_watcher/triage/noop.py +24 -0
- android_watcher-1.0.0/src/android_watcher/tui/__init__.py +1 -0
- android_watcher-1.0.0/src/android_watcher/tui/app.py +163 -0
- android_watcher-1.0.0/src/android_watcher/tui/configio.py +215 -0
- android_watcher-1.0.0/src/android_watcher/tui/screens.py +927 -0
- android_watcher-1.0.0/tests/conftest.py +16 -0
- android_watcher-1.0.0/tests/detect/__init__.py +0 -0
- android_watcher-1.0.0/tests/detect/test_android_sitemap.py +721 -0
- android_watcher-1.0.0/tests/detect/test_base.py +32 -0
- android_watcher-1.0.0/tests/detect/test_confirm_shared.py +129 -0
- android_watcher-1.0.0/tests/detect/test_content.py +194 -0
- android_watcher-1.0.0/tests/detect/test_feed.py +138 -0
- android_watcher-1.0.0/tests/detect/test_normalize.py +81 -0
- android_watcher-1.0.0/tests/detect/test_registry_autoload.py +23 -0
- android_watcher-1.0.0/tests/detect/test_registry_resolution.py +24 -0
- android_watcher-1.0.0/tests/detect/test_sitemap.py +356 -0
- android_watcher-1.0.0/tests/fixtures/claude_cli_envelope.json +62 -0
- android_watcher-1.0.0/tests/fixtures/content_after_chrome_only.html +11 -0
- android_watcher-1.0.0/tests/fixtures/content_after_real_change.html +11 -0
- android_watcher-1.0.0/tests/fixtures/content_before.html +11 -0
- android_watcher-1.0.0/tests/fixtures/content_js_shell.html +7 -0
- android_watcher-1.0.0/tests/fixtures/feed_guid_reuse.xml +12 -0
- android_watcher-1.0.0/tests/fixtures/feed_initial.xml +18 -0
- android_watcher-1.0.0/tests/fixtures/feed_updated_summary.xml +18 -0
- android_watcher-1.0.0/tests/fixtures/schedule/android-watcher.service +6 -0
- android_watcher-1.0.0/tests/fixtures/schedule/android-watcher.timer +9 -0
- android_watcher-1.0.0/tests/fixtures/schedule/launchd_daily.plist +22 -0
- android_watcher-1.0.0/tests/fixtures/sitemap_index.xml +9 -0
- android_watcher-1.0.0/tests/fixtures/sitemap_shard0.xml +11 -0
- android_watcher-1.0.0/tests/fixtures/sitemap_shard_i18n.xml +19 -0
- android_watcher-1.0.0/tests/fixtures/sitemap_simple.xml +11 -0
- android_watcher-1.0.0/tests/notify/__init__.py +0 -0
- android_watcher-1.0.0/tests/notify/snapshots/normal_email.html +7 -0
- android_watcher-1.0.0/tests/notify/snapshots/normal_email.txt +4 -0
- android_watcher-1.0.0/tests/notify/snapshots/normal_slack.json +25 -0
- android_watcher-1.0.0/tests/notify/test_base.py +35 -0
- android_watcher-1.0.0/tests/notify/test_email.py +270 -0
- android_watcher-1.0.0/tests/notify/test_html.py +113 -0
- android_watcher-1.0.0/tests/notify/test_render.py +348 -0
- android_watcher-1.0.0/tests/notify/test_slack.py +415 -0
- android_watcher-1.0.0/tests/notify/test_telegram.py +434 -0
- android_watcher-1.0.0/tests/run/__init__.py +0 -0
- android_watcher-1.0.0/tests/run/test_detect_and_persist.py +147 -0
- android_watcher-1.0.0/tests/run/test_run_once.py +650 -0
- android_watcher-1.0.0/tests/test_catalog.py +28 -0
- android_watcher-1.0.0/tests/test_catalog_data.py +71 -0
- android_watcher-1.0.0/tests/test_cli.py +280 -0
- android_watcher-1.0.0/tests/test_cli_schedule.py +119 -0
- android_watcher-1.0.0/tests/test_config.py +222 -0
- android_watcher-1.0.0/tests/test_configio.py +457 -0
- android_watcher-1.0.0/tests/test_configio_custom_source_roundtrip.py +81 -0
- android_watcher-1.0.0/tests/test_contributing_docs.py +24 -0
- android_watcher-1.0.0/tests/test_docs_scheduling.py +26 -0
- android_watcher-1.0.0/tests/test_doctor.py +287 -0
- android_watcher-1.0.0/tests/test_fetch.py +162 -0
- android_watcher-1.0.0/tests/test_group.py +121 -0
- android_watcher-1.0.0/tests/test_homebrew_formula.py +26 -0
- android_watcher-1.0.0/tests/test_lock.py +24 -0
- android_watcher-1.0.0/tests/test_models.py +116 -0
- android_watcher-1.0.0/tests/test_packaging.py +125 -0
- android_watcher-1.0.0/tests/test_rank.py +226 -0
- android_watcher-1.0.0/tests/test_readme_disclosures.py +28 -0
- android_watcher-1.0.0/tests/test_registry.py +57 -0
- android_watcher-1.0.0/tests/test_release_workflow.py +50 -0
- android_watcher-1.0.0/tests/test_schedule_crontab.py +53 -0
- android_watcher-1.0.0/tests/test_schedule_install.py +276 -0
- android_watcher-1.0.0/tests/test_schedule_plist.py +82 -0
- android_watcher-1.0.0/tests/test_schedule_status.py +134 -0
- android_watcher-1.0.0/tests/test_schedule_systemd.py +61 -0
- android_watcher-1.0.0/tests/test_seed.py +178 -0
- android_watcher-1.0.0/tests/test_seed_workflow.py +42 -0
- android_watcher-1.0.0/tests/test_store.py +385 -0
- android_watcher-1.0.0/tests/test_tui_smoke.py +321 -0
- android_watcher-1.0.0/tests/test_verify_catalog_smoke.py +28 -0
- android_watcher-1.0.0/tests/triage/__init__.py +0 -0
- android_watcher-1.0.0/tests/triage/test_base.py +55 -0
- android_watcher-1.0.0/tests/triage/test_claude_cli.py +307 -0
- android_watcher-1.0.0/tests/triage/test_claude_cli_prompt.py +154 -0
- android_watcher-1.0.0/tests/triage/test_noop.py +60 -0
- android_watcher-1.0.0/uv.lock +467 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
# Tabs, width 4, everywhere by default.
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
indent_style = tab
|
|
10
|
+
indent_size = 4
|
|
11
|
+
tab_width = 4
|
|
12
|
+
|
|
13
|
+
# Python line length matches [tool.ruff]; indentation is tabs (ruff format).
|
|
14
|
+
[*.py]
|
|
15
|
+
max_line_length = 100
|
|
16
|
+
|
|
17
|
+
# YAML forbids tab indentation (spec), so it must use spaces.
|
|
18
|
+
[*.{yml,yaml}]
|
|
19
|
+
indent_style = space
|
|
20
|
+
indent_size = 2
|
|
21
|
+
|
|
22
|
+
# Markdown keeps trailing whitespace (two trailing spaces = hard line break).
|
|
23
|
+
[*.md]
|
|
24
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something isn't working as documented
|
|
3
|
+
labels: [ "bug" ]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: Thanks for the report. Please REDACT secrets (SMTP password, Slack bot token, Telegram bot token) and private paths before pasting.
|
|
8
|
+
- type: textarea
|
|
9
|
+
id: what-happened
|
|
10
|
+
attributes:
|
|
11
|
+
label: What happened?
|
|
12
|
+
description: What went wrong, and what did you expect instead?
|
|
13
|
+
validations:
|
|
14
|
+
required: true
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: repro
|
|
17
|
+
attributes:
|
|
18
|
+
label: Steps to reproduce
|
|
19
|
+
placeholder: |
|
|
20
|
+
1. ...
|
|
21
|
+
2. ...
|
|
22
|
+
3. ...
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: input
|
|
26
|
+
id: version
|
|
27
|
+
attributes:
|
|
28
|
+
label: android-watcher version or commit
|
|
29
|
+
description: "`android-watcher --version` or the installed tag."
|
|
30
|
+
validations:
|
|
31
|
+
required: false
|
|
32
|
+
- type: textarea
|
|
33
|
+
id: env
|
|
34
|
+
attributes:
|
|
35
|
+
label: Environment
|
|
36
|
+
description: Install method (uv tool / pipx / Homebrew / source), OS, Python version, AI mode (claude_cli / off).
|
|
37
|
+
validations:
|
|
38
|
+
required: false
|
|
39
|
+
- type: textarea
|
|
40
|
+
id: logs
|
|
41
|
+
attributes:
|
|
42
|
+
label: Logs / output
|
|
43
|
+
description: Relevant output from `android-watcher doctor` or the failing run, with secrets redacted.
|
|
44
|
+
validations:
|
|
45
|
+
required: false
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Question / contact
|
|
4
|
+
url: https://github.com/krayong/android-watcher#contact
|
|
5
|
+
about: General questions that aren't bugs or feature requests (email androidwatcher@krayong.com).
|
|
6
|
+
- name: Security vulnerability
|
|
7
|
+
url: https://github.com/krayong/android-watcher/security/advisories/new
|
|
8
|
+
about: Report security issues privately, not as a public issue (see SECURITY.md).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest an improvement or a new source/channel
|
|
3
|
+
labels: [ "enhancement" ]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: Problem
|
|
9
|
+
description: What are you trying to do, and what's missing or awkward today?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed solution
|
|
16
|
+
description: What you'd like to see. For a new catalog source, link the official page and note its type (RSS feed, sitemap, or content page).
|
|
17
|
+
validations:
|
|
18
|
+
required: false
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: alternatives
|
|
21
|
+
attributes:
|
|
22
|
+
label: Alternatives considered
|
|
23
|
+
validations:
|
|
24
|
+
required: false
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR do and why? -->
|
|
4
|
+
|
|
5
|
+
## Testing
|
|
6
|
+
|
|
7
|
+
<!-- Describe how you verified the change. Note: tests run with `uv run pytest` (no live network; recorded fixtures only). -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] Tests added or updated for the change
|
|
12
|
+
- [ ] `uv run ruff check .` and `uv run ruff format .` pass with no errors
|
|
13
|
+
- [ ] Comments/docs state facts directly, with no pointers to scratch/planning notes
|
|
14
|
+
- [ ] Follows the guidelines in [CONTRIBUTING.md](../CONTRIBUTING.md)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
lint-and-test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
run: uv python install 3.11
|
|
24
|
+
|
|
25
|
+
- name: Sync dependencies
|
|
26
|
+
run: uv sync --frozen
|
|
27
|
+
|
|
28
|
+
- name: Ruff lint
|
|
29
|
+
run: uv run ruff check .
|
|
30
|
+
|
|
31
|
+
- name: Ruff format check
|
|
32
|
+
run: uv run ruff format --check .
|
|
33
|
+
|
|
34
|
+
- name: Pytest
|
|
35
|
+
run: uv run pytest -q
|
|
36
|
+
|
|
37
|
+
secret-scan:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
# Filesystem scan (not a git BASE..HEAD diff): the diff mode errors when
|
|
43
|
+
# BASE == HEAD, which happens on a single-commit push to main.
|
|
44
|
+
- name: Install TruffleHog
|
|
45
|
+
run: |
|
|
46
|
+
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \
|
|
47
|
+
| sh -s -- -b /usr/local/bin v3.88.1
|
|
48
|
+
|
|
49
|
+
- name: Scan for verified secrets
|
|
50
|
+
run: trufflehog filesystem . --only-verified --fail --no-update
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Manual, bump-driven release. Pick the semver part to bump; the job computes the
|
|
4
|
+
# new version from the current one, commits it to main, publishes to PyPI, refreshes
|
|
5
|
+
# the Homebrew formula, and tags the release last.
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
bump:
|
|
10
|
+
description: "Semver part to bump (relative to the current version)"
|
|
11
|
+
required: true
|
|
12
|
+
type: choice
|
|
13
|
+
options:
|
|
14
|
+
- patch
|
|
15
|
+
- minor
|
|
16
|
+
- major
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write # push the bump commit, the formula commit, and the tag
|
|
20
|
+
|
|
21
|
+
concurrency:
|
|
22
|
+
group: release
|
|
23
|
+
cancel-in-progress: false
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
release:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
|
|
29
|
+
environment: prod # holds the PYPI_TOKEN secret
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
ref: main
|
|
35
|
+
fetch-depth: 0 # full history so we can push back and tag
|
|
36
|
+
|
|
37
|
+
- name: Configure git as the release bot
|
|
38
|
+
run: |
|
|
39
|
+
set -euo pipefail
|
|
40
|
+
git config user.name "github-actions[bot]"
|
|
41
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
42
|
+
git fetch --tags --force
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@v5
|
|
46
|
+
|
|
47
|
+
- name: Set up Python
|
|
48
|
+
run: uv python install 3.11
|
|
49
|
+
|
|
50
|
+
- name: Bump version
|
|
51
|
+
id: bump
|
|
52
|
+
run: |
|
|
53
|
+
uv version --bump "${{ inputs.bump }}"
|
|
54
|
+
NEW=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
55
|
+
echo "version=$NEW" >> "$GITHUB_OUTPUT"
|
|
56
|
+
echo "Bumped ${{ inputs.bump }} -> $NEW"
|
|
57
|
+
|
|
58
|
+
- name: Run tests (no live network)
|
|
59
|
+
run: uv run pytest
|
|
60
|
+
|
|
61
|
+
- name: Build sdist and wheel
|
|
62
|
+
run: uv build
|
|
63
|
+
|
|
64
|
+
- name: Commit version bump to main
|
|
65
|
+
run: |
|
|
66
|
+
git add pyproject.toml uv.lock
|
|
67
|
+
git commit -m "chore: bump version to v${{ steps.bump.outputs.version }}"
|
|
68
|
+
git push origin HEAD:main
|
|
69
|
+
|
|
70
|
+
- name: Publish to PyPI
|
|
71
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
72
|
+
with:
|
|
73
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
74
|
+
|
|
75
|
+
- name: Update Homebrew formula
|
|
76
|
+
run: |
|
|
77
|
+
VERSION="${{ steps.bump.outputs.version }}"
|
|
78
|
+
# PyPI's JSON index lags publish by a few seconds; poll until the release shows up.
|
|
79
|
+
for i in $(seq 1 30); do
|
|
80
|
+
if curl -fsSL "https://pypi.org/pypi/android-watcher/${VERSION}/json" -o /tmp/pypi.json; then
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
echo "Waiting for PyPI to index ${VERSION} (attempt ${i})…"
|
|
84
|
+
sleep 10
|
|
85
|
+
done
|
|
86
|
+
read -r SDIST_URL SDIST_SHA < <(python3 - <<'PY'
|
|
87
|
+
import json
|
|
88
|
+
d = json.load(open("/tmp/pypi.json"))
|
|
89
|
+
s = next(u for u in d["urls"] if u["packagetype"] == "sdist")
|
|
90
|
+
print(s["url"], s["digests"]["sha256"])
|
|
91
|
+
PY
|
|
92
|
+
)
|
|
93
|
+
# Point the formula at the freshly published sdist + its hash.
|
|
94
|
+
python3 - "$SDIST_URL" "$SDIST_SHA" <<'PY'
|
|
95
|
+
import re, sys
|
|
96
|
+
url, sha = sys.argv[1], sys.argv[2]
|
|
97
|
+
p = "Formula/android-watcher.rb"
|
|
98
|
+
s = open(p).read()
|
|
99
|
+
s = re.sub(r' url ".*"', f' url "{url}"', s, count=1)
|
|
100
|
+
s = re.sub(r' sha256 ".*"', f' sha256 "{sha}"', s, count=1)
|
|
101
|
+
open(p, "w").write(s)
|
|
102
|
+
PY
|
|
103
|
+
# Regenerate every pinned dependency `resource` block against this release.
|
|
104
|
+
brew update-python-resources Formula/android-watcher.rb
|
|
105
|
+
git add Formula/android-watcher.rb
|
|
106
|
+
git commit -m "chore: update homebrew formula for v${VERSION}"
|
|
107
|
+
git push origin HEAD:main
|
|
108
|
+
|
|
109
|
+
- name: Tag the release
|
|
110
|
+
run: |
|
|
111
|
+
git tag "v${{ steps.bump.outputs.version }}"
|
|
112
|
+
git push origin "v${{ steps.bump.outputs.version }}"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: seed
|
|
2
|
+
|
|
3
|
+
# Regenerate the bundled baseline seed (src/android_watcher/seed/seed.sql.gz).
|
|
4
|
+
# This runs the full, polite, hours-long crawl of the catalog sources, so it is
|
|
5
|
+
# DELIBERATELY kept off the release path (release.yml just bundles whatever seed
|
|
6
|
+
# is committed). Trigger it manually from the Actions tab, or let the weekly
|
|
7
|
+
# schedule refresh it. You can also generate it locally and push:
|
|
8
|
+
# uv run python scripts/build_seed.py
|
|
9
|
+
on:
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
schedule:
|
|
13
|
+
- cron: "30 23 * * 0" # Mondays 05:00 IST (Sun 23:30 UTC)
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write # commit + push the refreshed seed
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: seed
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build-seed:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
|
|
26
|
+
# The crawl is throttled by a per-host crawl delay; bound it well under the
|
|
27
|
+
# 6h hard limit. The WIP-DB cache lets a re-run resume an interrupted crawl.
|
|
28
|
+
timeout-minutes: 350
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
with:
|
|
33
|
+
ref: main
|
|
34
|
+
|
|
35
|
+
- name: Install uv
|
|
36
|
+
uses: astral-sh/setup-uv@v5
|
|
37
|
+
|
|
38
|
+
- name: Set up Python
|
|
39
|
+
run: uv python install 3.11
|
|
40
|
+
|
|
41
|
+
- name: Sync dependencies
|
|
42
|
+
run: uv sync --frozen
|
|
43
|
+
|
|
44
|
+
# Persist the partial crawl DB so a retried run continues instead of
|
|
45
|
+
# restarting (baseline_all skips URLs already baselined).
|
|
46
|
+
- name: Restore crawl progress
|
|
47
|
+
uses: actions/cache@v4
|
|
48
|
+
with:
|
|
49
|
+
path: .seed-wip.db
|
|
50
|
+
key: seed-wip-${{ github.run_id }}
|
|
51
|
+
restore-keys: seed-wip-
|
|
52
|
+
|
|
53
|
+
- name: Build the baseline seed
|
|
54
|
+
run: uv run python scripts/build_seed.py --db .seed-wip.db
|
|
55
|
+
|
|
56
|
+
- name: Commit refreshed seed
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
git config user.name "github-actions[bot]"
|
|
60
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
61
|
+
git add src/android_watcher/seed/seed.sql.gz
|
|
62
|
+
if git diff --staged --quiet; then
|
|
63
|
+
echo "seed unchanged; nothing to commit"
|
|
64
|
+
else
|
|
65
|
+
git commit -m "chore: refresh baseline seed"
|
|
66
|
+
git push origin HEAD:main
|
|
67
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Project-specific
|
|
2
|
+
.idea/
|
|
3
|
+
|
|
4
|
+
# Python bytecode
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# Packaging / build
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
*.egg
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
wheels/
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
ENV/
|
|
22
|
+
# NOTE: uv.lock IS committed (pinned deps); do not ignore it.
|
|
23
|
+
|
|
24
|
+
# Test / coverage / type / lint caches
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.ruff_cache/
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
.pyright/
|
|
29
|
+
.coverage
|
|
30
|
+
.coverage.*
|
|
31
|
+
coverage.xml
|
|
32
|
+
htmlcov/
|
|
33
|
+
.tox/
|
|
34
|
+
.nox/
|
|
35
|
+
|
|
36
|
+
# Local env and secrets
|
|
37
|
+
.env
|
|
38
|
+
.env.*
|
|
39
|
+
|
|
40
|
+
# Editors
|
|
41
|
+
.vscode/
|
|
42
|
+
*.swp
|
|
43
|
+
|
|
44
|
+
# OS cruft
|
|
45
|
+
.DS_Store
|
|
46
|
+
Thumbs.db
|
|
47
|
+
|
|
48
|
+
# Planning and design docs (local-only working scratch, not shipped)
|
|
49
|
+
docs/superpowers/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Local lint + format + hygiene enforcement.
|
|
2
|
+
# Install once with: uv run pre-commit install
|
|
3
|
+
# Run on all files with: uv run pre-commit run --all-files
|
|
4
|
+
# The ruff version here is kept in sync with the pinned dev dependency.
|
|
5
|
+
repos:
|
|
6
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
7
|
+
rev: v0.15.18
|
|
8
|
+
hooks:
|
|
9
|
+
- id: ruff
|
|
10
|
+
name: ruff (lint)
|
|
11
|
+
args: [ --fix ]
|
|
12
|
+
- id: ruff-format
|
|
13
|
+
name: ruff (format)
|
|
14
|
+
|
|
15
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
16
|
+
rev: v5.0.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: trailing-whitespace
|
|
19
|
+
args: [ --markdown-linebreak-ext=md ]
|
|
20
|
+
- id: end-of-file-fixer
|
|
21
|
+
- id: check-json
|
|
22
|
+
- id: check-yaml
|
|
23
|
+
- id: check-merge-conflict
|
|
24
|
+
- id: check-added-large-files
|
|
25
|
+
|
|
26
|
+
- repo: https://github.com/trufflesecurity/trufflehog
|
|
27
|
+
rev: v3.88.1
|
|
28
|
+
hooks:
|
|
29
|
+
- id: trufflehog
|
|
30
|
+
name: Scan for secrets
|
|
31
|
+
entry: trufflehog git file://. --since-commit HEAD --fail
|
|
32
|
+
language: golang
|
|
33
|
+
pass_filenames: false
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Implementation guide for `android-watcher`. The rules here are the durable source
|
|
4
|
+
of truth. Build to these, not to memory.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
A self-hosted Python CLI that watches official Google Android sites, detects
|
|
9
|
+
real (not cosmetic) changes on a schedule, uses Claude to triage and describe
|
|
10
|
+
them, ranks the result, and delivers a digest to email or Slack. Single user per
|
|
11
|
+
install. Configured through a Textual TUI that writes a TOML file and installs a
|
|
12
|
+
native scheduled job.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync # install deps from the lockfile
|
|
18
|
+
uv run pytest # run tests (no live network; recorded fixtures only)
|
|
19
|
+
uv run pytest path::test -v # one test
|
|
20
|
+
uv run ruff check . # lint
|
|
21
|
+
uv run ruff format . # format
|
|
22
|
+
uv tool install . # install the CLI locally
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Package layout
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/android_watcher/
|
|
29
|
+
models.py # dataclasses, exceptions, Check, SignalType, INTERVAL_DELTA
|
|
30
|
+
config.py # Config + load_config (env interpolation, validation)
|
|
31
|
+
catalog/ # catalog.toml (shipped data) + load_catalog
|
|
32
|
+
store.py # SQLite: snapshots, changes, deliveries, digests, seen_feed_items, http_cache, run_state; seed import/export
|
|
33
|
+
seed/ # bundled baseline seed (seed.sql.gz) + apply_seed_if_empty
|
|
34
|
+
lock.py # single-instance run lock
|
|
35
|
+
fetch.py # async Fetcher: conditional GET, robots, backoff, sitemap cache
|
|
36
|
+
registry.py # generic name->class registry
|
|
37
|
+
detect/ # base + feed, android_sitemap, sitemap, content
|
|
38
|
+
rank.py # scoring, per-source caps, overflow
|
|
39
|
+
triage/ # base + claude_cli, noop
|
|
40
|
+
notify/ # base + render, email, slack
|
|
41
|
+
schedule.py # launchd / systemd / crontab install/remove/status
|
|
42
|
+
doctor.py # health checks
|
|
43
|
+
run.py # run_once pipeline
|
|
44
|
+
cli.py # `android-watcher` entrypoint
|
|
45
|
+
tui/ # Textual app + pure config<->TOML module
|
|
46
|
+
tests/ # mirrors src; tests/fixtures/ for recorded data
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Durable invariants
|
|
50
|
+
|
|
51
|
+
These are the rules that make the tool correct. They were hard-won; do not relax
|
|
52
|
+
them without understanding why they exist.
|
|
53
|
+
|
|
54
|
+
### Detection: candidate then confirm
|
|
55
|
+
|
|
56
|
+
- A feed item, a sitemap `lastmod` bump, or a content-hash change is a *candidate*, not a change. Confirm it against the
|
|
57
|
+
actual fetched page content before recording a change. `lastmod` alone never counts: Google bumps it on bulk
|
|
58
|
+
regenerations, template edits, and translation passes.
|
|
59
|
+
- A detector never emits a `Change` for a health problem (an empty/JS-only render, a path prefix that matches zero
|
|
60
|
+
sitemap URLs). It logs a warning and returns nothing; `doctor` surfaces the condition.
|
|
61
|
+
- The content detector refuses to baseline a page whose extracted text is below a small threshold (a client-rendered
|
|
62
|
+
shell), so it never silently hashes nothing forever.
|
|
63
|
+
- Feed dedupe keys on a stable identity: the Atom `<id>` verbatim, else a normalized link URL. Persist the full
|
|
64
|
+
seen-set. An existing item counts as changed only when its title+summary hash moves.
|
|
65
|
+
- The `android_sitemap` detector is host-agnostic: it parses a host's sitemap (a `<sitemapindex>` of shards, or a single
|
|
66
|
+
`<urlset>`) once per run, cached on the `Fetcher` keyed by the sitemap-index URL derived from each source's host (
|
|
67
|
+
`<scheme>://<host>/sitemap.xml`). Sources on the same host share one download (guarded by an `asyncio.Lock`,
|
|
68
|
+
conditional GET per shard); different hosts each get their own. It serves developer.android.com, source.android.com,
|
|
69
|
+
developers.google.com, kotlinlang.org, etc. — never download a host's shards per source.
|
|
70
|
+
- English only: locale-prefixed URLs (`/fr/...`, `/pt-br/...`) and `?hl=<non-en>` query variants are dropped at parse
|
|
71
|
+
time, so only canonical English pages are watched. Match the leading path segment against an explicit locale
|
|
72
|
+
allowlist, never a generic two-letter regex. Real sections like `/tv`, `/xr`, `/ai` start with two letters and must
|
|
73
|
+
not be mistaken for locales.
|
|
74
|
+
- Per-source filters (applied against the shared cached entry list): `path_prefix` (include; `""` = whole host),
|
|
75
|
+
`exclude_prefixes` (drop subtrees), `require_segment` (keep only URLs with a matching path segment, e.g. `android`),
|
|
76
|
+
and `reference_mode` (`keep` | `drop` | `index_only`). `index_only` keeps only reference index/summary pages — leaf in
|
|
77
|
+
`{package-summary, packages, classes, composables, modifiers}`, Kotlin-preferred (a Java reference page is dropped
|
|
78
|
+
when its `/reference/kotlin/...` twin exists), so the huge per-symbol class/function reference is excluded.
|
|
79
|
+
Most-specific-prefix-wins (scoped per host) routes a URL to its nested source; a `""` catch-all coexists with curated
|
|
80
|
+
sources for ranking weights.
|
|
81
|
+
- Version-dedup: URLs differing only by a dotted version segment (`9.4`) or a `?version=`/`?api=` query collapse to the
|
|
82
|
+
latest. Bare-integer paths (`/about/versions/14` vs `/15`) are untouched: those are distinct releases, not versions of
|
|
83
|
+
one page.
|
|
84
|
+
- Fetch-free first sight, then new-page detection: when a source has no baseline yet (first run / seed import), a
|
|
85
|
+
brand-new URL is baselined from its `lastmod` alone (empty `content_hash`, no fetch), with no "everything is new"
|
|
86
|
+
flood. Once a baseline exists, a never-seen URL is content-confirmed and reported as `Change(change_kind="new")`. An
|
|
87
|
+
already-baselined URL whose `lastmod` moves is content-confirmed; the first real fetch of a fetch-free baseline is
|
|
88
|
+
itself a silent capture (`confirm_candidate` treats an empty prior `content_hash` as first sight). `lastmod` alone
|
|
89
|
+
never emits a `Change`.
|
|
90
|
+
- All XML parsing uses `defusedxml`. Honor `robots.txt`. Send a descriptive User-Agent and a crawl delay.
|
|
91
|
+
|
|
92
|
+
### Pipeline: the delivery ledger
|
|
93
|
+
|
|
94
|
+
- `run_once` holds a single-instance lock. Overlapping runs exit immediately.
|
|
95
|
+
- The authoritative digest source is the ledger, never this-run detections. Rank `changes_for_digest(enabled_channels)`:
|
|
96
|
+
substantive changes not yet delivered to every enabled channel, at most one row per `(source_id, url)` (latest by
|
|
97
|
+
`detected_at`).
|
|
98
|
+
- `record_change` is idempotent on `(source_id, url, fetched_hash)`: it returns the existing row id and never resets a
|
|
99
|
+
verdict.
|
|
100
|
+
- `set_verdict` is write-once. Triage only touches rows with `verdict IS NULL`.
|
|
101
|
+
- When a ranked change is delivered, `supersede_older` marks older undelivered rows for the same `(source_id, url)` so a
|
|
102
|
+
page that changed twice yields one digest line, not a stale one.
|
|
103
|
+
- Delivery is per `(change, channel)`, recorded in `deliveries`. Send, then record the delivery transactionally. A
|
|
104
|
+
channel that already succeeded is never re-sent; a channel that failed is retried next run.
|
|
105
|
+
- An in-flight `digests` row is opened before sending and reconciled on the next startup: re-deliver the undelivered
|
|
106
|
+
channels, then commit. This closes the crash-between-send-and-commit window.
|
|
107
|
+
- First run baselines silently (no "everything is new" flood); the first digest after baseline is capped.
|
|
108
|
+
- A fresh DB imports the bundled baseline seed (`seed/seed.sql.gz`) before detecting: `apply_seed_if_empty` loads it
|
|
109
|
+
only when `snapshot_count() == 0`, via `INSERT OR IGNORE` so user data is never overwritten. The seed carries
|
|
110
|
+
snapshots + feed seen-set + HTTP validators, tagged with a `seed_date` in `run_state` (never `last_successful_run`).
|
|
111
|
+
The seed is generated by `scripts/build_seed.py` (the one expensive full-content crawl — run locally or via the `seed`
|
|
112
|
+
workflow, never in `release.yml`) and committed; the build bundles it via the wheel `artifacts` glob. When absent,
|
|
113
|
+
import is a no-op and the detectors baseline fetch-free instead. `doctor` surfaces the seed date and snapshot count.
|
|
114
|
+
- An empty "nothing notable" digest is sent at most once per catch-up window.
|
|
115
|
+
- Zero channels enabled: short-circuit before opening a digest, still mark the run successful.
|
|
116
|
+
- A missed cycle (machine asleep) is backfilled via `last_successful_run` and `INTERVAL_DELTA`, not dropped.
|
|
117
|
+
|
|
118
|
+
### AI / triage
|
|
119
|
+
|
|
120
|
+
- `claude_cli` shells out to `claude -p --output-format json`, strips a markdown code fence from the result before
|
|
121
|
+
parsing, and on any failure returns `TriageResult(unavailable=<reason>)` without raising. The digest still goes out,
|
|
122
|
+
with a visible "AI unavailable" banner.
|
|
123
|
+
- Fetched page content is untrusted. Wrap it in per-run nonce-fenced blocks, length-cap it, and instruct the model to
|
|
124
|
+
treat it as data, never instructions.
|
|
125
|
+
- `noop` (AI off) marks every change substantive with no description and does not filter.
|
|
126
|
+
|
|
127
|
+
### Config and secrets
|
|
128
|
+
|
|
129
|
+
- Paths come from `platformdirs`. The config file is written `0600`.
|
|
130
|
+
- String values support `${ENV_VAR}` interpolation on secret-bearing fields, resolved at load. The TUI editor loads with
|
|
131
|
+
`expand=False` so it preserves `${...}` literals and never crashes on an unset variable.
|
|
132
|
+
- Source selection: start from catalog entries with `enabled=True`; if the user's `enabled_sources` list is non-empty,
|
|
133
|
+
intersect with it; an empty or absent list means "use the catalog flags," not "none." Custom sources are always
|
|
134
|
+
watched; on id collision a custom source overrides the catalog. The TUI writes the reserved id `["__none__"]` to
|
|
135
|
+
mean "watch no catalog sources."
|
|
136
|
+
- SMTP enforces TLS and fails closed. The Slack bot token is a secret.
|
|
137
|
+
|
|
138
|
+
### Conventions
|
|
139
|
+
|
|
140
|
+
- Python 3.11+. Async lives only in `fetch.py` and detectors; everything else is synchronous. `run_once` drives the
|
|
141
|
+
async detectors with `asyncio.run`.
|
|
142
|
+
- Always `datetime.now(timezone.utc)`; never the naive `utcnow()`. The store coerces to UTC-aware at its boundary.
|
|
143
|
+
- Shared types, the four exceptions (`ConfigError`, `AlreadyRunning`, `Disallowed`, `NotifyError`), `Check`,
|
|
144
|
+
`SignalType`, and `INTERVAL_DELTA` are defined once in `models.py` and imported everywhere, which keeps the import
|
|
145
|
+
graph acyclic.
|
|
146
|
+
- Registries store classes. `get(name)` returns the class; callers instantiate it with no arguments. Unknown names raise
|
|
147
|
+
with a message listing the available ones.
|
|
148
|
+
- TDD: write the failing test first with real assertions, make it pass with the minimal real code, commit. No live
|
|
149
|
+
network in tests; use recorded fixtures under `tests/fixtures/`. A test must exercise the guarantee, not echo the
|
|
150
|
+
implementation.
|
|
151
|
+
|
|
152
|
+
## No Stale Plan References
|
|
153
|
+
|
|
154
|
+
**IMPORTANT:** After any refactor or new feature, do NOT leave references in code or markdown to what the plan
|
|
155
|
+
was — no "per the plan", no "according to decision N", no plan phase or task numbers, no pointer to the
|
|
156
|
+
planning or design doc. Those planning docs are scratch that won't survive, so a citation to them becomes a
|
|
157
|
+
stale, dangling reference the moment they're deleted. State the fact or rule directly; if a decision's
|
|
158
|
+
rationale matters, write the rationale itself, not a pointer to where it was decided.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our pledge
|
|
4
|
+
|
|
5
|
+
This project adopts the [Contributor Covenant](https://www.contributor-covenant.org),
|
|
6
|
+
version 2.1, as its Code of Conduct. We are committed to providing a welcoming,
|
|
7
|
+
respectful, and harassment-free experience for everyone who participates in this
|
|
8
|
+
project, regardless of background or identity.
|
|
9
|
+
|
|
10
|
+
The full text of the Contributor Covenant v2.1 is available at:
|
|
11
|
+
https://www.contributor-covenant.org/version/2/1/code_of_conduct/
|
|
12
|
+
|
|
13
|
+
By participating in this project, you agree to uphold its standards: be
|
|
14
|
+
considerate and respectful in all interactions, welcome differing viewpoints,
|
|
15
|
+
accept constructive feedback gracefully, and focus on what is best for the
|
|
16
|
+
community.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
This Code of Conduct applies within all project spaces (the repository, issue
|
|
21
|
+
tracker, pull requests, and discussions) and when an individual represents the
|
|
22
|
+
project in public spaces.
|
|
23
|
+
|
|
24
|
+
## Reporting
|
|
25
|
+
|
|
26
|
+
If you experience or witness unacceptable behavior, report it confidentially to
|
|
27
|
+
the project maintainers by opening a private/security advisory on the project's
|
|
28
|
+
GitHub repository, or by emailing **androidwatcher@krayong.com**. All reports are
|
|
29
|
+
reviewed and investigated promptly and fairly. Maintainers are obligated to
|
|
30
|
+
respect the privacy and security of anyone who reports an incident.
|
|
31
|
+
|
|
32
|
+
## Enforcement
|
|
33
|
+
|
|
34
|
+
Maintainers are responsible for clarifying and enforcing these standards and may
|
|
35
|
+
take appropriate, fair corrective action in response to behavior they deem
|
|
36
|
+
inappropriate, threatening, offensive, or harmful, including removing comments,
|
|
37
|
+
commits, code, and contributions, or temporarily or permanently banning a
|
|
38
|
+
contributor for behaviors they judge to violate this Code of Conduct.
|
|
39
|
+
|
|
40
|
+
Enforcement follows the Contributor Covenant v2.1 enforcement guidelines, linked
|
|
41
|
+
above.
|