ForageFacebook 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/.github/workflows/ci.yml +4 -25
  2. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/.gitignore +5 -0
  3. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/AGENTS.md +9 -7
  4. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/CHANGELOG.md +12 -1
  5. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/CONTRIBUTING.md +6 -4
  6. foragefacebook-2.2.0/Makefile +14 -0
  7. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/PKG-INFO +15 -6
  8. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/README.md +14 -4
  9. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/docs/capabilities.md +1 -1
  10. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/pyproject.toml +1 -2
  11. foragefacebook-2.2.0/scripts/live-e2e.py +57 -0
  12. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/__init__.py +1 -1
  13. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/cli.py +11 -2
  14. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/uv.lock +2 -110
  15. foragefacebook-2.1.0/tests/__init__.py +0 -1
  16. foragefacebook-2.1.0/tests/conftest.py +0 -43
  17. foragefacebook-2.1.0/tests/fixtures/comment_simple.html +0 -7
  18. foragefacebook-2.1.0/tests/fixtures/comment_with_replies.html +0 -23
  19. foragefacebook-2.1.0/tests/fixtures/post_long_content.html +0 -13
  20. foragefacebook-2.1.0/tests/fixtures/post_realistic.html +0 -54
  21. foragefacebook-2.1.0/tests/fixtures/post_simple.html +0 -13
  22. foragefacebook-2.1.0/tests/fixtures/post_sponsored.html +0 -7
  23. foragefacebook-2.1.0/tests/fixtures/post_with_emoji.html +0 -12
  24. foragefacebook-2.1.0/tests/fixtures/post_with_media.html +0 -15
  25. foragefacebook-2.1.0/tests/fixtures/post_with_see_more.html +0 -12
  26. foragefacebook-2.1.0/tests/test_auth.py +0 -48
  27. foragefacebook-2.1.0/tests/test_cli.py +0 -161
  28. foragefacebook-2.1.0/tests/test_exporter.py +0 -669
  29. foragefacebook-2.1.0/tests/test_marketplace.py +0 -56
  30. foragefacebook-2.1.0/tests/test_models.py +0 -158
  31. foragefacebook-2.1.0/tests/test_parser.py +0 -362
  32. foragefacebook-2.1.0/tests/test_regressions.py +0 -352
  33. foragefacebook-2.1.0/tests/test_scraper.py +0 -473
  34. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/.github/FUNDING.yml +0 -0
  35. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/.github/dependabot.yml +0 -0
  36. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/.github/workflows/publish.yml +0 -0
  37. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/CLAUDE.md +0 -0
  38. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/LICENSE +0 -0
  39. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/SECURITY.md +0 -0
  40. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/docs/research/2026-09-20-cli-portfolio-audit.md +0 -0
  41. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/docs/research/2026-09-20-cli-portfolio-repairs.md +0 -0
  42. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/scripts/release.sh +0 -0
  43. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/auth.py +0 -0
  44. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/exporter.py +0 -0
  45. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/models.py +0 -0
  46. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/parser.py +0 -0
  47. {foragefacebook-2.1.0 → foragefacebook-2.2.0}/src/forage/scraper.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
@@ -12,11 +12,10 @@ 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
+ uv run ruff check src/ scripts/ # Lint
18
+ uv run ruff format src/ scripts/ # Format
20
19
  ```
21
20
 
22
21
  ## Manual Testing
@@ -59,7 +58,10 @@ Random viewport sizes, `human_delay()` with variance, real Chromium browser, ses
59
58
 
60
59
  ### Test Structure
61
60
 
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.
61
+ Use `make test` for real CLI reads through the saved Facebook session. Install Chromium first.
62
+ Set `FORAGE_E2E_GROUP` to test a group; otherwise the test searches Marketplace for computers.
63
+ Keep unit tests, mock tests, and fake-server suites out of this repo. CI runs static checks and package builds.
64
+ Live tests must not log in, change credentials, or write to Facebook.
63
65
 
64
66
  ## Fragile Code — Check Here First When Things Break
65
67
 
@@ -99,7 +101,7 @@ Steps:
99
101
  1. Update `version` in both `pyproject.toml` and `src/forage/__init__.py`
100
102
  2. Run `uv lock --no-config` — refresh locked editable package metadata without leaking user-level uv settings
101
103
  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
104
+ 4. Run `uv lock --check --no-config` and `uv run ruff format src/ scripts/` — CI enforces formatting
103
105
  5. Commit: `git commit -m "chore: release vX.Y.Z"`
104
106
  6. Push a feature branch and open a PR. Merge only after all required checks pass. Never push directly to `master`.
105
107
  7. Tag: `git tag -a vX.Y.Z -m "Release vX.Y.Z"` then `git push origin vX.Y.Z`
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-10-10
11
+
12
+ ### Changed
13
+
14
+ - Replace unit and synthetic browser tests with read-only live CLI checks.
15
+ - Keep lint, type checks, and package build/install checks in CI.
16
+ - Remove pytest and the Node test harness.
17
+ - Add common `make check`, `make test`, and `make build` entry points.
18
+ - Add `forage version` beside the existing `--version` flag.
19
+
10
20
  ## [2.1.0] - 2026-09-20
11
21
 
12
22
  ### Fixed
@@ -237,7 +247,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
237
247
  - Sensitive files excluded via `.gitignore`
238
248
  - Security guidelines in SECURITY.md
239
249
 
240
- [Unreleased]: https://github.com/jwmoss/forage/compare/v2.1.0...HEAD
250
+ [Unreleased]: https://github.com/jwmoss/forage/compare/v2.2.0...HEAD
251
+ [2.2.0]: https://github.com/jwmoss/forage/compare/v2.1.0...v2.2.0
241
252
  [2.1.0]: https://github.com/jwmoss/forage/compare/v2.0.0...v2.1.0
242
253
  [2.0.0]: https://github.com/jwmoss/forage/compare/v1.2.1...v2.0.0
243
254
  [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,15 +56,16 @@ 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. Ensure tests pass (`make check`; run `make test` with a saved session)
59
60
  5. Ensure type checking passes (`uv run ty check src/forage/`)
60
61
  6. Commit with a descriptive message
61
62
  7. Push and open a pull request
62
63
 
63
64
  ## Testing
64
65
 
65
- - Write tests for new functionality
66
- - Use pytest fixtures for common test data
66
+ - Verify behavior through the real CLI against Facebook.
67
+ - Keep live checks read-only and use the existing session.
68
+ - Do not add unit tests, mock tests, or fake-server suites.
67
69
  - Mock external dependencies (Playwright, network)
68
70
  - Aim for good coverage of edge cases
69
71
 
@@ -0,0 +1,14 @@
1
+ .PHONY: check test 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
+ build:
14
+ 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.2.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
@@ -319,13 +318,23 @@ uv sync --extra dev
319
318
  # Run type checker
320
319
  uv run ty check src/
321
320
 
322
- # Install Chromium for offline DOM tests
321
+ # Run static checks and build the package
322
+ make check
323
+
324
+ # Install Chromium for live reads
323
325
  uv run playwright install chromium
324
326
 
325
- # Run tests; browser fixtures block network access
326
- uv run pytest
327
+ # Read Marketplace through the saved Facebook session
328
+ make test
329
+
330
+ # Read one post from a group you can access
331
+ FORAGE_E2E_GROUP=your-group-slug make test
327
332
  ```
328
333
 
334
+ Live tests run the real CLI and Facebook pages. They do not use fixtures or mock servers.
335
+ They need an existing login session. CI runs static checks and package build/install checks.
336
+
337
+
329
338
  ## Architecture
330
339
 
331
340
  ```
@@ -344,7 +353,7 @@ src/forage/
344
353
  - Rate limiting may require slower scraping
345
354
  - Individual reaction types not broken out (only total)
346
355
  - Session lifetime depends on Facebook; use `forage login` when authentication fails
347
- - English page labels and supported DOM fixtures define the tested parser scope
356
+ - English page labels define the supported parser scope
348
357
  - Previously overwritten SQLite comments require a new scrape; upgrades cannot recover them
349
358
 
350
359
  ## Roadmap
@@ -284,13 +284,23 @@ uv sync --extra dev
284
284
  # Run type checker
285
285
  uv run ty check src/
286
286
 
287
- # Install Chromium for offline DOM tests
287
+ # Run static checks and build the package
288
+ make check
289
+
290
+ # Install Chromium for live reads
288
291
  uv run playwright install chromium
289
292
 
290
- # Run tests; browser fixtures block network access
291
- uv run pytest
293
+ # Read Marketplace through the saved Facebook session
294
+ make test
295
+
296
+ # Read one post from a group you can access
297
+ FORAGE_E2E_GROUP=your-group-slug make test
292
298
  ```
293
299
 
300
+ Live tests run the real CLI and Facebook pages. They do not use fixtures or mock servers.
301
+ They need an existing login session. CI runs static checks and package build/install checks.
302
+
303
+
294
304
  ## Architecture
295
305
 
296
306
  ```
@@ -309,7 +319,7 @@ src/forage/
309
319
  - Rate limiting may require slower scraping
310
320
  - Individual reaction types not broken out (only total)
311
321
  - Session lifetime depends on Facebook; use `forage login` when authentication fails
312
- - English page labels and supported DOM fixtures define the tested parser scope
322
+ - English page labels define the supported parser scope
313
323
  - Previously overwritten SQLite comments require a new scrape; upgrades cannot recover them
314
324
 
315
325
  ## Roadmap
@@ -34,4 +34,4 @@ The current audit cannot retrieve that page or current [Marketplace documentatio
34
34
  Current replacement availability and exhaustive REST coverage therefore remain unknown. Do not infer private APIs from DOM fields.
35
35
 
36
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.
37
+ Verify selector changes through read-only live CLI requests. 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.2.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,57 @@
1
+ #!/usr/bin/env python3
2
+ """Check real Facebook reads through the installed CLI and saved session."""
3
+
4
+ import json
5
+ import os
6
+ import shlex
7
+ import subprocess
8
+ import sys
9
+
10
+
11
+ def main():
12
+ cli = shlex.split(os.environ.get("FORAGE_BIN", "forage"))
13
+
14
+ def call(*args):
15
+ result = subprocess.run(
16
+ [*cli, "--no-color", *args], capture_output=True, text=True, timeout=120
17
+ )
18
+ if result.returncode:
19
+ raise RuntimeError(
20
+ f"{args[0]} failed with exit {result.returncode}; run the command to inspect the error"
21
+ )
22
+ return json.loads(result.stdout)
23
+
24
+ doctor = call("doctor")
25
+ if not doctor.get("browser_installed") or not doctor.get("session_exists"):
26
+ raise RuntimeError(
27
+ "Install Chromium and save a Facebook session before live tests"
28
+ )
29
+ group = os.environ.get("FORAGE_E2E_GROUP")
30
+ if group:
31
+ result = call("scrape", group, "--limit", "1", "--skip-comments", "--no-input")
32
+ rows = result.get("posts")
33
+ else:
34
+ result = call(
35
+ "marketplace", "computer", "--limit", "1", "--max-candidates", "3"
36
+ )
37
+ rows = result.get("listings")
38
+ if not isinstance(rows, list) or not rows or not rows[0].get("id"):
39
+ raise RuntimeError(
40
+ "Live read returned no verified record; inspect access and page layout"
41
+ )
42
+ print("PASS real Facebook CLI read")
43
+
44
+
45
+ if __name__ == "__main__":
46
+ try:
47
+ main()
48
+ except (
49
+ RuntimeError,
50
+ ValueError,
51
+ KeyError,
52
+ TypeError,
53
+ OSError,
54
+ subprocess.TimeoutExpired,
55
+ ) as error:
56
+ print(f"FAIL {error}", file=sys.stderr)
57
+ 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.2.0"
@@ -61,6 +61,12 @@ def main(ctx: Context, verbose: bool, quiet: bool, no_color: bool):
61
61
  console.no_color = True
62
62
 
63
63
 
64
+ @main.command()
65
+ def version() -> None:
66
+ """Print version information."""
67
+ click.echo(f"forage, version {__version__}")
68
+
69
+
64
70
  @main.command()
65
71
  @click.option(
66
72
  "--browser",
@@ -162,7 +168,10 @@ def marketplace(
162
168
 
163
169
  json_output = result.model_dump_json(indent=2)
164
170
  if output:
165
- output.write_text(json_output, encoding="utf-8")
171
+ try:
172
+ output.write_text(json_output, encoding="utf-8")
173
+ except OSError as error:
174
+ raise click.ClickException(str(error)) from error
166
175
  if not ctx.quiet:
167
176
  console.print(f"[green]Output written to {output}[/green]")
168
177
  else:
@@ -493,7 +502,7 @@ def export_saved(
493
502
  top_comments=top_comments,
494
503
  min_pain_score=min_pain_score,
495
504
  )
496
- except ValidationError as error:
505
+ except (ValidationError, UnicodeDecodeError) as error:
497
506
  raise click.ClickException("Input is not a group scrape JSON result") from error
498
507
  except (OSError, sqlite3.Error) as error:
499
508
  raise click.ClickException(str(error)) from error
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
@@ -32,21 +32,9 @@ wheels = [
32
32
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
33
33
  ]
34
34
 
35
- [[package]]
36
- name = "exceptiongroup"
37
- version = "1.3.1"
38
- source = { registry = "https://pypi.org/simple" }
39
- dependencies = [
40
- { name = "typing-extensions" },
41
- ]
42
- sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
43
- wheels = [
44
- { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
45
- ]
46
-
47
35
  [[package]]
48
36
  name = "foragefacebook"
49
- version = "2.1.0"
37
+ version = "2.2.0"
50
38
  source = { editable = "." }
51
39
  dependencies = [
52
40
  { name = "click" },
@@ -57,7 +45,6 @@ dependencies = [
57
45
 
58
46
  [package.optional-dependencies]
59
47
  dev = [
60
- { name = "pytest" },
61
48
  { name = "ruff" },
62
49
  { name = "ty" },
63
50
  ]
@@ -67,7 +54,6 @@ requires-dist = [
67
54
  { name = "click", specifier = ">=8.0" },
68
55
  { name = "playwright", specifier = ">=1.40" },
69
56
  { name = "pydantic", specifier = ">=2.0" },
70
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
71
57
  { name = "rich", specifier = ">=13.0" },
72
58
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
73
59
  { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a6" },
@@ -129,15 +115,6 @@ wheels = [
129
115
  { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
130
116
  ]
131
117
 
132
- [[package]]
133
- name = "iniconfig"
134
- version = "2.3.0"
135
- source = { registry = "https://pypi.org/simple" }
136
- sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
137
- wheels = [
138
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
139
- ]
140
-
141
118
  [[package]]
142
119
  name = "markdown-it-py"
143
120
  version = "4.0.0"
@@ -159,15 +136,6 @@ wheels = [
159
136
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
160
137
  ]
161
138
 
162
- [[package]]
163
- name = "packaging"
164
- version = "25.0"
165
- source = { registry = "https://pypi.org/simple" }
166
- sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
167
- wheels = [
168
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
169
- ]
170
-
171
139
  [[package]]
172
140
  name = "playwright"
173
141
  version = "1.57.0"
@@ -187,15 +155,6 @@ wheels = [
187
155
  { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" },
188
156
  ]
189
157
 
190
- [[package]]
191
- name = "pluggy"
192
- version = "1.6.0"
193
- source = { registry = "https://pypi.org/simple" }
194
- sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
195
- wheels = [
196
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
197
- ]
198
-
199
158
  [[package]]
200
159
  name = "pydantic"
201
160
  version = "2.12.5"
@@ -350,24 +309,6 @@ wheels = [
350
309
  { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
351
310
  ]
352
311
 
353
- [[package]]
354
- name = "pytest"
355
- version = "9.0.2"
356
- source = { registry = "https://pypi.org/simple" }
357
- dependencies = [
358
- { name = "colorama", marker = "sys_platform == 'win32'" },
359
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
360
- { name = "iniconfig" },
361
- { name = "packaging" },
362
- { name = "pluggy" },
363
- { name = "pygments" },
364
- { name = "tomli", marker = "python_full_version < '3.11'" },
365
- ]
366
- sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
367
- wheels = [
368
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
369
- ]
370
-
371
312
  [[package]]
372
313
  name = "rich"
373
314
  version = "14.2.0"
@@ -406,55 +347,6 @@ wheels = [
406
347
  { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
407
348
  ]
408
349
 
409
- [[package]]
410
- name = "tomli"
411
- version = "2.3.0"
412
- source = { registry = "https://pypi.org/simple" }
413
- sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
414
- wheels = [
415
- { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
416
- { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
417
- { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
418
- { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
419
- { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
420
- { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
421
- { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
422
- { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
423
- { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
424
- { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
425
- { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
426
- { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
427
- { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
428
- { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
429
- { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
430
- { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
431
- { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
432
- { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
433
- { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
434
- { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
435
- { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
436
- { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
437
- { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
438
- { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
439
- { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
440
- { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
441
- { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
442
- { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
443
- { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
444
- { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
445
- { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
446
- { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
447
- { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
448
- { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
449
- { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
450
- { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
451
- { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
452
- { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
453
- { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
454
- { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
455
- { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
456
- ]
457
-
458
350
  [[package]]
459
351
  name = "ty"
460
352
  version = "0.0.10"
@@ -1 +0,0 @@
1
- """Tests for forage."""
@@ -1,43 +0,0 @@
1
- """Use Playwright's real DOM for selector fixtures, with network access disabled."""
2
-
3
- from pathlib import Path
4
- from unittest.mock import MagicMock
5
-
6
- import pytest
7
- from playwright.sync_api import sync_playwright
8
-
9
- FIXTURES_DIR = Path(__file__).parent / "fixtures"
10
-
11
-
12
- @pytest.fixture(scope="session")
13
- def browser():
14
- with sync_playwright() as playwright:
15
- browser = playwright.chromium.launch()
16
- yield browser
17
- browser.close()
18
-
19
-
20
- @pytest.fixture
21
- def dom_page(browser):
22
- context = browser.new_context()
23
- context.route("**/*", lambda route: route.abort())
24
- page = context.new_page()
25
- yield page
26
- context.close()
27
-
28
-
29
- @pytest.fixture
30
- def mock_page():
31
- return MagicMock()
32
-
33
-
34
- @pytest.fixture
35
- def simple_post_element(dom_page):
36
- dom_page.set_content((FIXTURES_DIR / "post_simple.html").read_text())
37
- return dom_page.query_selector('[role="article"]')
38
-
39
-
40
- @pytest.fixture
41
- def simple_comment_element(dom_page):
42
- dom_page.set_content((FIXTURES_DIR / "comment_simple.html").read_text())
43
- return dom_page.query_selector('[role="article"]')
@@ -1,7 +0,0 @@
1
- <!-- Simple comment mock -->
2
- <div role="article">
3
- <strong>Bob Wilson</strong>
4
- <a href="https://www.facebook.com/bob.wilson" role="link">Bob Wilson</a>
5
- <div dir="auto">Great recommendation! I love that place.</div>
6
- <span aria-label="5 reactions">5</span>
7
- </div>