ForageFacebook 2.1.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/.github/workflows/ci.yml +4 -25
  2. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/.gitignore +5 -0
  3. foragefacebook-2.3.0/.python-version +1 -0
  4. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/AGENTS.md +12 -7
  5. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/CHANGELOG.md +34 -1
  6. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/CONTRIBUTING.md +16 -6
  7. foragefacebook-2.3.0/Makefile +25 -0
  8. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/PKG-INFO +34 -8
  9. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/README.md +33 -6
  10. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/docs/capabilities.md +19 -8
  11. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/pyproject.toml +1 -2
  12. foragefacebook-2.3.0/scripts/live-e2e.py +164 -0
  13. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/__init__.py +1 -1
  14. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/cli.py +30 -3
  15. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/parser.py +47 -26
  16. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/scraper.py +144 -189
  17. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/uv.lock +2 -110
  18. foragefacebook-2.1.0/tests/__init__.py +0 -1
  19. foragefacebook-2.1.0/tests/conftest.py +0 -43
  20. foragefacebook-2.1.0/tests/fixtures/comment_simple.html +0 -7
  21. foragefacebook-2.1.0/tests/fixtures/comment_with_replies.html +0 -23
  22. foragefacebook-2.1.0/tests/fixtures/post_long_content.html +0 -13
  23. foragefacebook-2.1.0/tests/fixtures/post_realistic.html +0 -54
  24. foragefacebook-2.1.0/tests/fixtures/post_simple.html +0 -13
  25. foragefacebook-2.1.0/tests/fixtures/post_sponsored.html +0 -7
  26. foragefacebook-2.1.0/tests/fixtures/post_with_emoji.html +0 -12
  27. foragefacebook-2.1.0/tests/fixtures/post_with_media.html +0 -15
  28. foragefacebook-2.1.0/tests/fixtures/post_with_see_more.html +0 -12
  29. foragefacebook-2.1.0/tests/test_auth.py +0 -48
  30. foragefacebook-2.1.0/tests/test_cli.py +0 -161
  31. foragefacebook-2.1.0/tests/test_exporter.py +0 -669
  32. foragefacebook-2.1.0/tests/test_marketplace.py +0 -56
  33. foragefacebook-2.1.0/tests/test_models.py +0 -158
  34. foragefacebook-2.1.0/tests/test_parser.py +0 -362
  35. foragefacebook-2.1.0/tests/test_regressions.py +0 -352
  36. foragefacebook-2.1.0/tests/test_scraper.py +0 -473
  37. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/.github/FUNDING.yml +0 -0
  38. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/.github/dependabot.yml +0 -0
  39. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/.github/workflows/publish.yml +0 -0
  40. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/CLAUDE.md +0 -0
  41. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/LICENSE +0 -0
  42. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/SECURITY.md +0 -0
  43. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/docs/research/2026-09-20-cli-portfolio-audit.md +0 -0
  44. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/docs/research/2026-09-20-cli-portfolio-repairs.md +0 -0
  45. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/scripts/release.sh +0 -0
  46. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/auth.py +0 -0
  47. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/exporter.py +0 -0
  48. {foragefacebook-2.1.0 → foragefacebook-2.3.0}/src/forage/models.py +0 -0
@@ -16,26 +16,6 @@ concurrency:
16
16
  cancel-in-progress: ${{ github.ref_type != 'tag' }}
17
17
 
18
18
  jobs:
19
- test:
20
- name: test (${{ matrix.python-version }})
21
- runs-on: ubuntu-latest
22
- timeout-minutes: 15
23
- strategy:
24
- fail-fast: false
25
- matrix:
26
- python-version: ["3.10", "3.14"]
27
- steps:
28
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
29
- with:
30
- persist-credentials: false
31
- - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
32
- with:
33
- version: "0.12.17"
34
- python-version: ${{ matrix.python-version }}
35
- - run: uv sync --locked --all-extras --no-config
36
- - run: uv run --locked --no-config playwright install --with-deps chromium
37
- - run: uv run --locked --no-config pytest tests/ -q
38
-
39
19
  quality:
40
20
  runs-on: ubuntu-latest
41
21
  timeout-minutes: 15
@@ -51,8 +31,8 @@ jobs:
51
31
  - name: Check source and lockfile
52
32
  run: |
53
33
  uv lock --check --no-config
54
- uv run --locked --no-config ruff check src/ tests/
55
- uv run --locked --no-config ruff format --check src/ tests/
34
+ uv run --locked --no-config ruff check src/ scripts/
35
+ uv run --locked --no-config ruff format --check src/ scripts/
56
36
  uv run --locked --no-config ty check src/forage/
57
37
  - name: Audit workflows and release helper
58
38
  run: |
@@ -70,12 +50,11 @@ jobs:
70
50
 
71
51
  check:
72
52
  if: always()
73
- needs: [test, quality]
53
+ needs: [quality]
74
54
  runs-on: ubuntu-latest
75
55
  timeout-minutes: 2
76
56
  steps:
77
57
  - name: Require all checks
78
58
  env:
79
- TEST_RESULT: ${{ needs.test.result }}
80
59
  QUALITY_RESULT: ${{ needs.quality.result }}
81
- run: test "$TEST_RESULT" = success && test "$QUALITY_RESULT" = success
60
+ run: test "$QUALITY_RESULT" = success
@@ -60,3 +60,8 @@ output/
60
60
  .env
61
61
  .env.local
62
62
  .env.*.local
63
+
64
+ # CLI end-to-end runner
65
+ /node_modules/
66
+ /.e2e/
67
+ !package-lock.json
@@ -0,0 +1 @@
1
+ 3.14
@@ -12,11 +12,11 @@ Forage is a CLI tool for scraping private Facebook groups to analyze community d
12
12
  uv sync # Install deps (always use uv, not pip/python)
13
13
  uv run forage --help # Run CLI
14
14
  uv run ty check src/ # Type check (Astral's ty)
15
- uv run pytest # Run all tests
16
- uv run pytest tests/test_parser.py # Run a single test file
17
- uv run pytest -k "test_name" # Run a single test by name
18
- uv run ruff check src/ tests/ # Lint
19
- uv run ruff format src/ tests/ # Auto-format (CI enforces this)
15
+ make check # Lint, type check, and build
16
+ make test # Read-only live Facebook checks
17
+ make pre-pr # Static and live checks; set FORAGE_E2E_GROUP
18
+ uv run ruff check src/ scripts/ # Lint
19
+ uv run ruff format src/ scripts/ # Format
20
20
  ```
21
21
 
22
22
  ## Manual Testing
@@ -59,7 +59,12 @@ Random viewport sizes, `human_delay()` with variance, real Chromium browser, ses
59
59
 
60
60
  ### Test Structure
61
61
 
62
- Tests in `tests/` with HTML fixtures in `tests/fixtures/` (realistic Facebook HTML snippets). `conftest.py` loads fixtures into real Playwright Chromium pages with network access blocked. Install Chromium before tests.
62
+ Use `make test` for real CLI reads through the saved Facebook session. Install Chromium first.
63
+ Set `FORAGE_E2E_GROUP` to test a group; otherwise the test searches Marketplace for computers.
64
+ Before you create a PR, run `FORAGE_E2E_GROUP=<active-group> make pre-pr`.
65
+ Set `FORAGE_E2E_UNTIL=YYYY-MM-DD` to also check a historical window with known posts.
66
+ Keep unit tests, mock tests, and fake-server suites out of this repo. CI runs static checks and package builds.
67
+ Live tests must not log in, change credentials, or write to Facebook.
63
68
 
64
69
  ## Fragile Code — Check Here First When Things Break
65
70
 
@@ -99,7 +104,7 @@ Steps:
99
104
  1. Update `version` in both `pyproject.toml` and `src/forage/__init__.py`
100
105
  2. Run `uv lock --no-config` — refresh locked editable package metadata without leaking user-level uv settings
101
106
  3. Update `CHANGELOG.md` — add version section under `## [Unreleased]`, update comparison links at bottom
102
- 4. Run `uv lock --check --no-config` and `uv run ruff format src/ tests/` — CI enforces formatting
107
+ 4. Run `uv lock --check --no-config` and `uv run ruff format src/ scripts/` — CI enforces formatting
103
108
  5. Commit: `git commit -m "chore: release vX.Y.Z"`
104
109
  6. Push a feature branch and open a PR. Merge only after all required checks pass. Never push directly to `master`.
105
110
  7. Tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` then `git push origin vX.Y.Z`
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.3.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - Add Marketplace sort choices and support more currency-symbol price formats, inspired by PR #50.
15
+ - Add local `make pre-pr` checks for live Marketplace sorts, group comments, date windows, and saved exports.
16
+
17
+ ### Fixed
18
+
19
+ - Preserve comment nesting and remove the fallback that treats arbitrary post text as comments.
20
+ - Read relative comment-author links and restrict permalink comments to the requested post.
21
+ - Continue historical searches past newer posts and include posts labeled "Just now" at the default cutoff.
22
+ - Parse compact reaction totals and recognize `multi_permalinks` post links.
23
+ - Skip repeated Marketplace candidates before text parsing and stop before an unnecessary final group scroll.
24
+ - Convert the CLI's miles to Facebook's kilometer radius and use the verified nearest-first sort value.
25
+ - Select Python 3.14 for local development, matching CI.
26
+
27
+ ### Thanks
28
+
29
+ - Thanks to [@kha-faz](https://github.com/kha-faz) for the currency and Marketplace sort contributions in [PR #50](https://github.com/jwmoss/forage/pull/50).
30
+
31
+ ## [2.2.0] - 2026-10-10
32
+
33
+ ### Changed
34
+
35
+ - Replace unit and synthetic browser tests with read-only live CLI checks.
36
+ - Keep lint, type checks, and package build/install checks in CI.
37
+ - Remove pytest and the Node test harness.
38
+ - Add common `make check`, `make test`, and `make build` entry points.
39
+ - Add `forage version` beside the existing `--version` flag.
40
+
10
41
  ## [2.1.0] - 2026-09-20
11
42
 
12
43
  ### Fixed
@@ -237,7 +268,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
237
268
  - Sensitive files excluded via `.gitignore`
238
269
  - Security guidelines in SECURITY.md
239
270
 
240
- [Unreleased]: https://github.com/jwmoss/forage/compare/v2.1.0...HEAD
271
+ [Unreleased]: https://github.com/jwmoss/forage/compare/v2.3.0...HEAD
272
+ [2.3.0]: https://github.com/jwmoss/forage/compare/v2.2.0...v2.3.0
273
+ [2.2.0]: https://github.com/jwmoss/forage/compare/v2.1.0...v2.2.0
241
274
  [2.1.0]: https://github.com/jwmoss/forage/compare/v2.0.0...v2.1.0
242
275
  [2.0.0]: https://github.com/jwmoss/forage/compare/v1.2.1...v2.0.0
243
276
  [1.2.1]: https://github.com/jwmoss/forage/compare/v1.2.0...v1.2.1
@@ -22,7 +22,8 @@ Thank you for your interest in contributing to Forage!
22
22
 
23
23
  4. Run tests:
24
24
  ```bash
25
- uv run pytest tests/ -v
25
+ make check
26
+ make test
26
27
  ```
27
28
 
28
29
  5. Run type checker:
@@ -55,17 +56,26 @@ Example: `feat: add SQLite export option`
55
56
  1. Fork the repository
56
57
  2. Create a feature branch (`git checkout -b feat/my-feature`)
57
58
  3. Make your changes
58
- 4. Ensure tests pass (`uv run pytest tests/`)
59
+ 4. Run the local PR checks with a saved session:
60
+ ```bash
61
+ FORAGE_E2E_GROUP=your-group-slug FORAGE_E2E_UNTIL=YYYY-MM-DD make pre-pr
62
+ ```
63
+ Select an active group with visible reply threads and a historical window with known posts.
64
+ `FORAGE_E2E_UNTIL` is optional. It tests the seven days before the selected inclusive end date.
59
65
  5. Ensure type checking passes (`uv run ty check src/forage/`)
60
66
  6. Commit with a descriptive message
61
67
  7. Push and open a pull request
62
68
 
63
69
  ## Testing
64
70
 
65
- - Write tests for new functionality
66
- - Use pytest fixtures for common test data
67
- - Mock external dependencies (Playwright, network)
68
- - Aim for good coverage of edge cases
71
+ - Verify behavior through the real CLI against Facebook.
72
+ - Keep live checks read-only and use the existing session.
73
+ - Do not add unit tests, mock tests, or fake-server suites.
74
+ - Run `make test-marketplace` to check all five sort orders against Facebook's selected controls.
75
+ - Run `FORAGE_E2E_GROUP=your-group-slug make test-group` to check posts, comments, and real-data exports.
76
+ - Run `make pre-pr` with the group variables above before you create a PR.
77
+ - Keep session files and private records out of commits and PR descriptions.
78
+ - Live checks need suitable records. A passing run does not prove rare cases that the live pages do not contain.
69
79
 
70
80
  ## Reporting Issues
71
81
 
@@ -0,0 +1,25 @@
1
+ .PHONY: check test test-marketplace test-group test-all pre-pr build
2
+
3
+ check:
4
+ uv lock --check --no-config
5
+ uv run --locked --no-config ruff check src/ scripts/
6
+ uv run --locked --no-config ruff format --check src/ scripts/
7
+ uv run --locked --no-config ty check src/forage/
8
+ uv build --no-config
9
+
10
+ test:
11
+ uv run --locked --no-config python scripts/live-e2e.py
12
+
13
+ test-marketplace:
14
+ env -u FORAGE_E2E_GROUP uv run --locked --no-config python scripts/live-e2e.py
15
+
16
+ test-group:
17
+ @test -n "$$FORAGE_E2E_GROUP" || { echo 'Set FORAGE_E2E_GROUP to an active group you can access.' >&2; exit 2; }
18
+ uv run --locked --no-config python scripts/live-e2e.py
19
+
20
+ test-all: test-marketplace test-group
21
+
22
+ pre-pr: check test-all
23
+
24
+ build:
25
+ uv build --no-config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ForageFacebook
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: CLI tool to scrape posts, comments, and reactions from private Facebook groups
5
5
  Project-URL: Homepage, https://github.com/jwmoss/forage
6
6
  Project-URL: Repository, https://github.com/jwmoss/forage
@@ -28,7 +28,6 @@ Requires-Dist: playwright>=1.40
28
28
  Requires-Dist: pydantic>=2.0
29
29
  Requires-Dist: rich>=13.0
30
30
  Provides-Extra: dev
31
- Requires-Dist: pytest>=8.0; extra == 'dev'
32
31
  Requires-Dist: ruff>=0.8; extra == 'dev'
33
32
  Requires-Dist: ty>=0.0.1a6; extra == 'dev'
34
33
  Description-Content-Type: text/markdown
@@ -178,11 +177,19 @@ Commands:
178
177
 
179
178
  ```bash
180
179
  uv run forage marketplace "rtx 4070" --city wilmington --radius 40 --limit 20 -o listings.json
180
+ uv run forage marketplace "computer" --sort-by price_ascend
181
181
  ```
182
182
 
183
183
  `--limit` counts accepted listings. `--max-candidates` bounds the search cost, including rejected candidates; its default is 200.
184
184
  Forage excludes distant listings and listings whose coordinates it cannot verify. City is the search center, not a city-only filter.
185
- The current parser supports electronics with dollar prices or “Free” labels. Search relevance follows Facebook's results.
185
+ The parser accepts “Free” and prices with `$`, `£`, `€`, `¥`, `₹`, `₩`, or `₽` symbols.
186
+ It accepts prefixes such as `CA$` and `R$`, trailing symbols such as `80 €`, and space-separated digits.
187
+ Prices remain display strings. Currency conversion, currency-code-only prices, and localized free labels are unsupported.
188
+ Search relevance follows Facebook's results.
189
+
190
+ `--sort-by` accepts `creation_time_descend` (default), `suggested`, `price_ascend`, `price_descend`, or `distance_ascend`.
191
+ These values request Facebook's sort order. They do not sort the collected subset locally.
192
+ The CLI radius uses miles; Forage converts it to kilometers for Facebook's search URL.
186
193
 
187
194
  ### Local diagnosis and saved exports
188
195
 
@@ -319,13 +326,32 @@ uv sync --extra dev
319
326
  # Run type checker
320
327
  uv run ty check src/
321
328
 
322
- # Install Chromium for offline DOM tests
329
+ # Run static checks and build the package
330
+ make check
331
+
332
+ # Install Chromium for live reads
323
333
  uv run playwright install chromium
324
334
 
325
- # Run tests; browser fixtures block network access
326
- uv run pytest
335
+ # Read Marketplace through the saved Facebook session
336
+ make test
337
+
338
+ # Read posts and comments from an active group you can access
339
+ FORAGE_E2E_GROUP=your-group-slug make test
340
+
341
+ # Run static checks, all Marketplace sorts, group comments, and saved exports before a PR
342
+ FORAGE_E2E_GROUP=your-group-slug make pre-pr
343
+
344
+ # Also check a historical window with known posts (seven days through the inclusive end date)
345
+ FORAGE_E2E_GROUP=your-group-slug FORAGE_E2E_UNTIL=2026-10-29 make pre-pr
327
346
  ```
328
347
 
348
+ Live tests run the real CLI and Facebook pages. They do not use fixtures or mock servers.
349
+ They need an existing login session. CI runs static checks and package build/install checks.
350
+ Group checks require an active group with visible reply threads. The checks use temporary local files for exports.
351
+ Marketplace checks confirm Facebook's selected sort control for each CLI result URL.
352
+ Live data changes. These checks do not cover a rare parser case unless the selected pages contain it.
353
+
354
+
329
355
  ## Architecture
330
356
 
331
357
  ```
@@ -344,12 +370,12 @@ src/forage/
344
370
  - Rate limiting may require slower scraping
345
371
  - Individual reaction types not broken out (only total)
346
372
  - Session lifetime depends on Facebook; use `forage login` when authentication fails
347
- - English page labels and supported DOM fixtures define the tested parser scope
373
+ - English page labels define the supported parser scope
348
374
  - Previously overwritten SQLite comments require a new scrape; upgrades cannot recover them
349
375
 
350
376
  ## Roadmap
351
377
 
352
- Prioritize sanitized DOM fixtures and verified selector updates. Add incremental collection only after identity and completeness checks support it.
378
+ Prioritize read-only live regression checks and verified selector updates. Add incremental collection only after identity and completeness checks support it.
353
379
  Use shell loops for batches, system scheduling for recurring commands, and SQLite or jq for analysis.
354
380
 
355
381
  ### Contributing
@@ -143,11 +143,19 @@ Commands:
143
143
 
144
144
  ```bash
145
145
  uv run forage marketplace "rtx 4070" --city wilmington --radius 40 --limit 20 -o listings.json
146
+ uv run forage marketplace "computer" --sort-by price_ascend
146
147
  ```
147
148
 
148
149
  `--limit` counts accepted listings. `--max-candidates` bounds the search cost, including rejected candidates; its default is 200.
149
150
  Forage excludes distant listings and listings whose coordinates it cannot verify. City is the search center, not a city-only filter.
150
- The current parser supports electronics with dollar prices or “Free” labels. Search relevance follows Facebook's results.
151
+ The parser accepts “Free” and prices with `$`, `£`, `€`, `¥`, `₹`, `₩`, or `₽` symbols.
152
+ It accepts prefixes such as `CA$` and `R$`, trailing symbols such as `80 €`, and space-separated digits.
153
+ Prices remain display strings. Currency conversion, currency-code-only prices, and localized free labels are unsupported.
154
+ Search relevance follows Facebook's results.
155
+
156
+ `--sort-by` accepts `creation_time_descend` (default), `suggested`, `price_ascend`, `price_descend`, or `distance_ascend`.
157
+ These values request Facebook's sort order. They do not sort the collected subset locally.
158
+ The CLI radius uses miles; Forage converts it to kilometers for Facebook's search URL.
151
159
 
152
160
  ### Local diagnosis and saved exports
153
161
 
@@ -284,13 +292,32 @@ uv sync --extra dev
284
292
  # Run type checker
285
293
  uv run ty check src/
286
294
 
287
- # Install Chromium for offline DOM tests
295
+ # Run static checks and build the package
296
+ make check
297
+
298
+ # Install Chromium for live reads
288
299
  uv run playwright install chromium
289
300
 
290
- # Run tests; browser fixtures block network access
291
- uv run pytest
301
+ # Read Marketplace through the saved Facebook session
302
+ make test
303
+
304
+ # Read posts and comments from an active group you can access
305
+ FORAGE_E2E_GROUP=your-group-slug make test
306
+
307
+ # Run static checks, all Marketplace sorts, group comments, and saved exports before a PR
308
+ FORAGE_E2E_GROUP=your-group-slug make pre-pr
309
+
310
+ # Also check a historical window with known posts (seven days through the inclusive end date)
311
+ FORAGE_E2E_GROUP=your-group-slug FORAGE_E2E_UNTIL=2026-10-29 make pre-pr
292
312
  ```
293
313
 
314
+ Live tests run the real CLI and Facebook pages. They do not use fixtures or mock servers.
315
+ They need an existing login session. CI runs static checks and package build/install checks.
316
+ Group checks require an active group with visible reply threads. The checks use temporary local files for exports.
317
+ Marketplace checks confirm Facebook's selected sort control for each CLI result URL.
318
+ Live data changes. These checks do not cover a rare parser case unless the selected pages contain it.
319
+
320
+
294
321
  ## Architecture
295
322
 
296
323
  ```
@@ -309,12 +336,12 @@ src/forage/
309
336
  - Rate limiting may require slower scraping
310
337
  - Individual reaction types not broken out (only total)
311
338
  - Session lifetime depends on Facebook; use `forage login` when authentication fails
312
- - English page labels and supported DOM fixtures define the tested parser scope
339
+ - English page labels define the supported parser scope
313
340
  - Previously overwritten SQLite comments require a new scrape; upgrades cannot recover them
314
341
 
315
342
  ## Roadmap
316
343
 
317
- Prioritize sanitized DOM fixtures and verified selector updates. Add incremental collection only after identity and completeness checks support it.
344
+ Prioritize read-only live regression checks and verified selector updates. Add incremental collection only after identity and completeness checks support it.
318
345
  Use shell loops for batches, system scheduling for recurring commands, and SQLite or jq for analysis.
319
346
 
320
347
  ### Contributing
@@ -1,16 +1,16 @@
1
1
  # Forage capability contract
2
2
 
3
- Updated: 2026-09-20. This file describes the tested application behavior, not exhaustive Facebook coverage.
3
+ Updated: 2026-11-03. This file describes observed application behavior, not exhaustive Facebook coverage.
4
4
 
5
5
  | Surface | Verified locally | Known limits |
6
6
  | --- | --- | --- |
7
7
  | Login | Browser state storage with owner-only POSIX permissions | Manual Facebook login; session validity requires Facebook access |
8
8
  | Doctor | Browser executable, session existence, permission checks | No session contents read; no browser launch; no network check |
9
- | Group posts | Supported HTML fixtures, full visible paragraphs, nested-comment exclusion, supported expansion controls, permalinks | English labels; Facebook layout changes; media and attachment extraction absent |
9
+ | Group posts | Live authenticated CLI reads with native post IDs and known timestamps | English labels; Facebook layout changes; media and attachment extraction absent |
10
10
  | Dates | Inclusive calendar end dates, reversed-range rejection, relative timestamp parsing | Local system/browser timezone; month/year relative times remain approximate |
11
- | Comments | Native IDs, fallback IDs scoped to posts, supported reply collection | All-comment coverage remains unverified; identical fallback comments within one post can collide; comment timestamps remain unknown |
11
+ | Comments | Live native IDs, relative author links, and reply-parent relationships; fallback IDs scoped to posts | All-comment coverage remains unverified; identical fallback comments within one post can collide; comment timestamps remain unknown |
12
12
  | Reactions | Supported total-count labels | Missing and skipped counts retain legacy zero defaults; no promise of complete per-type counts |
13
- | Marketplace | Electronics, dollar/free prices, title/location, accepted result limit, strict radius check bound to the requested listing ID | Fixed category; fuzzy Facebook relevance; approximate coordinates; unbound or invalid coordinates are excluded; no exhaustive search guarantee |
13
+ | Marketplace | Live listings and all five selected sort controls; mile-to-kilometer URL conversion; strict radius check bound to the requested listing ID | Fixed category; fuzzy Facebook relevance; approximate coordinates; unbound or invalid coordinates are excluded; no exhaustive search guarantee |
14
14
  | Export | Group JSON to JSON, LLM JSON, CSV, SQLite without login | Keep original JSON for diagnostics; old overwritten comments cannot be recovered automatically |
15
15
 
16
16
  ## Result diagnostics
@@ -30,8 +30,19 @@ Meta's [Content Library announcement](https://about.fb.com/news/2023/11/new-tool
30
30
  It does not establish private-group or general personal Marketplace API access.
31
31
 
32
32
  The [Graph API v19 changelog](https://developers.facebook.com/docs/graph-api/changelog/version19.0/) is the authority for historical Groups API removal.
33
- The current audit cannot retrieve that page or current [Marketplace documentation](https://developers.facebook.com/docs/marketplace/) because Meta returns 429 responses.
34
- Current replacement availability and exhaustive REST coverage therefore remain unknown. Do not infer private APIs from DOM fields.
33
+ The September 2026 audit cannot retrieve that page or the [Marketplace documentation](https://developers.facebook.com/docs/marketplace/) because Meta returns 429 responses.
34
+ This update does not recheck replacement API availability. Do not infer private APIs from DOM fields.
35
35
 
36
- All new selector checks use synthetic, network-blocked Chromium pages. No live authenticated Facebook session validates this change.
37
- Use sanitized fixtures from a confirmed failure before changing selectors. Never publish session state or private group content in test fixtures.
36
+ ## Local regression checks
37
+
38
+ Run `FORAGE_E2E_GROUP=<active-group> FORAGE_E2E_UNTIL=YYYY-MM-DD make pre-pr` before a PR.
39
+ The date is optional. Select a historical window with known posts.
40
+ The checks use the saved session and real CLI reads. They require visible reply threads and verify Facebook's selected sort controls,
41
+ post/comment records, and CSV/SQLite exports of the collected data. CI remains static and does not use Facebook credentials.
42
+
43
+ On November 3, the same historical request returns zero posts on the base revision after nine rejected newer posts.
44
+ The fix reaches a matching post after 63 newer candidates. Live group, export, and all five Marketplace sort checks pass.
45
+ Direct parser checks cover the default "Just now" cutoff, compact reaction counts, and supported currency-symbol forms.
46
+ Those direct checks do not establish live coverage for every rare label or locale.
47
+
48
+ Never publish session state or private group content.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ForageFacebook"
7
- version = "2.1.0"
7
+ version = "2.3.0"
8
8
  description = "CLI tool to scrape posts, comments, and reactions from private Facebook groups"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -42,7 +42,6 @@ Changelog = "https://github.com/jwmoss/forage/blob/master/CHANGELOG.md"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  dev = [
45
- "pytest>=8.0",
46
45
  "ruff>=0.8",
47
46
  "ty>=0.0.1a6",
48
47
  ]
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+ """Check real Facebook reads and exports through the CLI and saved session."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import csv
7
+ import json
8
+ import os
9
+ import re
10
+ import shlex
11
+ import sqlite3
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ from datetime import date, datetime, timedelta
16
+ from pathlib import Path
17
+
18
+ from playwright.sync_api import sync_playwright
19
+
20
+ SORT_LABELS = {
21
+ "creation_time_descend": "Date listed: Newest first",
22
+ "suggested": "Suggested",
23
+ "price_ascend": "Price: Lowest first",
24
+ "price_descend": "Price: Highest first",
25
+ "distance_ascend": "Distance: Nearest first",
26
+ }
27
+
28
+
29
+ def call(*args: str) -> dict:
30
+ """Run the real CLI without exposing private records on failure."""
31
+ cli = shlex.split(os.environ.get("FORAGE_BIN", "forage"))
32
+ result = subprocess.run(
33
+ [*cli, "--no-color", *args], capture_output=True, text=True, timeout=300
34
+ )
35
+ if result.returncode:
36
+ raise RuntimeError(
37
+ f"{args[0]} failed with exit {result.returncode}; run the command to inspect the error"
38
+ )
39
+ return json.loads(result.stdout) if result.stdout.strip() else {}
40
+
41
+
42
+ def check_group(group: str) -> None:
43
+ """Check real post/comment reads, saved exports, and an optional date window."""
44
+ result = call("scrape", group, "--limit", "3", "--no-input")
45
+ posts = result["posts"]
46
+ assert posts and len(posts) <= 3, "No group posts or post limit exceeded"
47
+ assert len({post["id"] for post in posts}) == len(posts), "Duplicate posts"
48
+ comments = []
49
+ for post in posts:
50
+ pending = list(post["comments"])
51
+ seen = set()
52
+ while pending:
53
+ comment = pending.pop()
54
+ assert comment["id"] not in seen, "Duplicate comment or reply"
55
+ assert comment["content"], "Empty comment"
56
+ assert comment["content"] != post["content"], (
57
+ "Post text appears as a comment"
58
+ )
59
+ seen.add(comment["id"])
60
+ comments.append(comment)
61
+ pending.extend(comment["replies"])
62
+ assert comments, "Use an active group with visible comments for this check"
63
+ assert any(c["replies"] for c in comments), "Use a group with visible reply threads"
64
+ assert any(c["author"]["name"] != "Unknown" for c in comments), "No comment authors"
65
+
66
+ with tempfile.TemporaryDirectory(prefix="forage-live-") as directory:
67
+ root = Path(directory)
68
+ source = root / "posts.json"
69
+ source.write_text(json.dumps(result), encoding="utf-8")
70
+ call("export", str(source), "--format", "csv", "-o", str(root / "posts.csv"))
71
+ with (root / "posts.csv").open() as handle:
72
+ assert len(list(csv.DictReader(handle))) == len(posts), "CSV loses posts"
73
+ with (root / "posts.comments.csv").open() as handle:
74
+ assert len(list(csv.DictReader(handle))) == len(comments), (
75
+ "CSV loses comments"
76
+ )
77
+ call("export", str(source), "--format", "sqlite", "-o", str(root / "posts.db"))
78
+ with sqlite3.connect(root / "posts.db") as db:
79
+ assert db.execute("SELECT COUNT(*) FROM posts").fetchone()[0] == len(posts)
80
+ assert db.execute("SELECT COUNT(*) FROM comments").fetchone()[0] == len(
81
+ comments
82
+ )
83
+ print(
84
+ "PASS live group posts, comments, replies, and CSV/SQLite exports", flush=True
85
+ )
86
+
87
+ until = os.environ.get("FORAGE_E2E_UNTIL")
88
+ if until:
89
+ end = date.fromisoformat(until)
90
+ start = end - timedelta(days=7)
91
+ history = call(
92
+ "scrape",
93
+ group,
94
+ "--limit",
95
+ "1",
96
+ "--skip-comments",
97
+ "--no-input",
98
+ "--since",
99
+ start.isoformat(),
100
+ "--until",
101
+ until,
102
+ )
103
+ assert history["posts"], "No historical record; use a window with known posts"
104
+ for post in history["posts"]:
105
+ assert post["timestamp"], "Historical check needs a known timestamp"
106
+ assert start <= datetime.fromisoformat(post["timestamp"]).date() <= end
107
+ print("PASS live historical date window", flush=True)
108
+
109
+
110
+ def check_marketplace(session_path: str) -> None:
111
+ """Check each sort through the CLI and Facebook's selected UI control."""
112
+ with sync_playwright() as playwright:
113
+ browser = playwright.chromium.launch()
114
+ page = browser.new_context(storage_state=session_path).new_page()
115
+ try:
116
+ for order, label in SORT_LABELS.items():
117
+ result = call(
118
+ "marketplace",
119
+ "computer",
120
+ "--limit",
121
+ "1",
122
+ "--max-candidates",
123
+ "10",
124
+ "--sort-by",
125
+ order,
126
+ )
127
+ assert len(result["listings"]) == 1, "No accepted Marketplace listing"
128
+ assert result["listings"][0]["id"], "Missing listing ID"
129
+ assert result["diagnostics"]["candidates_seen"] <= 10, (
130
+ "Candidate limit exceeded"
131
+ )
132
+ page.goto(result["search_url"], wait_until="domcontentloaded")
133
+ page.wait_for_selector('a[href*="/marketplace/item/"]')
134
+ radio = page.get_by_role("radio", name=label, exact=True)
135
+ if not radio.count():
136
+ page.get_by_role("button", name=re.compile(r"^Sort by")).click()
137
+ assert radio.get_attribute("aria-checked") == "true", (
138
+ f"Facebook ignores {order}"
139
+ )
140
+ print(f"PASS live Marketplace {order}", flush=True)
141
+ finally:
142
+ browser.close()
143
+
144
+
145
+ def main() -> None:
146
+ """Verify the local setup, then read the selected live Facebook surface."""
147
+ doctor = call("doctor")
148
+ if not doctor.get("browser_installed") or not doctor.get("session_exists"):
149
+ raise RuntimeError(
150
+ "Install Chromium and save a Facebook session before live tests"
151
+ )
152
+ group = os.environ.get("FORAGE_E2E_GROUP")
153
+ if group:
154
+ check_group(group)
155
+ else:
156
+ check_marketplace(doctor["session_path"])
157
+
158
+
159
+ if __name__ == "__main__":
160
+ try:
161
+ main()
162
+ except Exception as error:
163
+ print(f"FAIL {type(error).__name__}: {error}", file=sys.stderr)
164
+ sys.exit(1)
@@ -1,3 +1,3 @@
1
1
  """Forage: CLI tool to scrape private Facebook groups."""
2
2
 
3
- __version__ = "2.1.0"
3
+ __version__ = "2.3.0"