Almaatla commited on
Commit
a604513
·
unverified ·
2 Parent(s): 33657f85a8a721

Safer concurrent deploys, pinned dependencies, and a CONTRIBUTING guide (#5)

Browse files
.github/workflows/deploy-to-hf.yml CHANGED
@@ -41,4 +41,10 @@ jobs:
41
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
42
  run: |
43
  git remote add space https://hf-actions-bot:${HF_TOKEN}@huggingface.co/spaces/OrganizedProgrammers/SERPent
44
- git push --force space HEAD:main
 
 
 
 
 
 
 
41
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
42
  run: |
43
  git remote add space https://hf-actions-bot:${HF_TOKEN}@huggingface.co/spaces/OrganizedProgrammers/SERPent
44
+ # Fetch first so --force-with-lease has a remote-tracking ref to
45
+ # compare against - without this it either fails every run or
46
+ # (depending on git version) silently behaves like plain --force.
47
+ # This makes the push fail instead of silently overwriting anyone
48
+ # else who deployed to this same Space since our last fetch.
49
+ git fetch space main
50
+ git push --force-with-lease space HEAD:main
.github/workflows/test.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Tests
2
+
3
+ # Runs the suite on every pull request targeting main, so a change gets
4
+ # feedback before merge instead of only at deploy time. deploy-to-hf.yml
5
+ # runs its own copy of this same gate right before every push to the Space
6
+ # (belt-and-suspenders: never trust a check that ran before the current
7
+ # state existed, the same principle behind that workflow's
8
+ # --force-with-lease push).
9
+ on:
10
+ pull_request:
11
+ branches:
12
+ - main
13
+
14
+ jobs:
15
+ test:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.11"
25
+
26
+ - name: Install dependencies
27
+ run: pip install -r requirements-dev.txt
28
+
29
+ - name: Install Playwright's Chromium
30
+ run: playwright install --with-deps chromium
31
+
32
+ - name: Run tests
33
+ run: pytest
CONTRIBUTING.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to SERPent
2
+
3
+ This file describes how this codebase is developed, not just how to run it.
4
+ It exists because the project went from zero tests to a real safety net in
5
+ one focused pass, and the point of that work is lost if the next change
6
+ skips the same discipline. It ships as a normal file in this repo, which
7
+ means it also lands on the Hugging Face Space on every deploy (see
8
+ "Deployment" below) — it isn't GitHub-only documentation.
9
+
10
+ ## Setup
11
+
12
+ ```bash
13
+ python3 -m venv .venv && source .venv/bin/activate
14
+ pip install -r requirements-dev.txt
15
+ playwright install chromium # only if not already present on the machine
16
+ ```
17
+
18
+ Dependencies in `requirements.txt` / `requirements-dev.txt` are pinned to
19
+ specific tested ranges, not left bare — see the comment at the top of
20
+ `requirements.txt` for the policy and how to bump one. This matters more
21
+ than it looks: coming back to this project after any gap in time, a bare
22
+ `pip install` with no pins would silently grab whatever's newest on PyPI
23
+ that day, and you'd have no way to tell "my change broke this" from "a
24
+ dependency update broke this."
25
+
26
+ ## Running tests
27
+
28
+ ```bash
29
+ pytest
30
+ ```
31
+
32
+ 97 tests, a few seconds, fully offline. All outbound HTTP is mocked with
33
+ `respx`; the Playwright-driven scrapers (Bing, Brave, Google Scholar,
34
+ Google Patents search) run against a real headless Chromium but navigate to
35
+ local fixture HTML instead of the live sites — see `tests/helpers.py` for
36
+ how and why. Nothing here depends on, or can trip, any external site's
37
+ anti-bot measures.
38
+
39
+ **Important limitation to keep in mind:** this suite protects against
40
+ regressions in this code. It cannot catch a live site changing its markup,
41
+ because the fixtures are frozen snapshots of the selector contract as of
42
+ when they were written — they'll stay green forever even if Bing's HTML
43
+ changes tomorrow. After any real gap in development, or if a scraper starts
44
+ returning empty/wrong results in production, run a live smoke test against
45
+ the deployed MCP server or REST API before assuming the code is at fault:
46
+
47
+ ```
48
+ search(["some query"])
49
+ scrap_patent("US10000000B2") # or any real, known-good patent id
50
+ ```
51
+
52
+ If a backend's selectors have drifted, that's a normal, expected kind of
53
+ bug here — fix it the same way as anything else (see below): update the
54
+ fixture HTML to match the new real markup, confirm the test fails against
55
+ the current selectors, fix the selectors, confirm green.
56
+
57
+ ## The development loop (TDD, not test-after)
58
+
59
+ Every change in this codebase's history followed the same loop, and new
60
+ changes should too:
61
+
62
+ 1. **Write the test first.** For new behavior, write it against the
63
+ behavior you want and confirm it fails (red) for the right reason - not
64
+ a typo, an import error, or a fixture bug.
65
+ 2. **For a refactor** (behavior should stay identical), write
66
+ characterization tests for the *current* behavior first if none exist,
67
+ confirm they pass against the unchanged code, and only then refactor
68
+ under that safety net. `app.py` had zero tests before its endpoint
69
+ deduplication; the tests were added and confirmed green against the old
70
+ code before a single line of the refactor happened.
71
+ 3. **Implement the minimal change** to make the new/updated test pass.
72
+ 4. **Run the full suite**, not just the new test - a shared helper's bug
73
+ should show up as multiple failures, and that's a sign it's finding real
74
+ things, not a nuisance.
75
+ 5. **Distrust a test that never fails.** When a test's value is in
76
+ question, deliberately break the code it's supposed to protect and
77
+ confirm the test actually catches it, then revert. This caught a real
78
+ gap once already: a circuit breaker test that looked correct was
79
+ actually vacuous until this step exposed it.
80
+ 6. **Watch for shared mutable state across tests.** A few characterization
81
+ tests in this codebase turned out to be recording real state into a
82
+ module-level singleton (a circuit breaker) with nothing resetting it
83
+ between tests - harmless by luck at the time, but the kind of thing that
84
+ silently breaks an unrelated, later test once one more test is added.
85
+ Give shared singletons a fresh instance per test (an autouse fixture,
86
+ or explicit monkeypatching) rather than relying on the real one.
87
+ 7. **Validate risky changes for real, not just by review.** The one Docker
88
+ change in this project's history that could plausibly have broken the
89
+ live deployment (running the container as non-root, which changes
90
+ whether Chromium's sandbox can start at all) was verified by actually
91
+ building and running the image locally and confirming a real scrape
92
+ request completed - not by reasoning about it from the Dockerfile alone.
93
+
94
+ ## CI / deployment
95
+
96
+ - **`.github/workflows/test.yml`** runs the full suite on every pull
97
+ request against `main`, so a change gets feedback before merge.
98
+ - **`.github/workflows/deploy-to-hf.yml`** runs the same suite again
99
+ (`test` job) on every push to `main`, and only then (`needs: test`)
100
+ pushes the repo to the Hugging Face Space (`deploy` job) with
101
+ `git push --force-with-lease`. That flag matters: a plain `--force` push
102
+ is a blind overwrite with no way to tell if someone else pushed to the
103
+ same Space in the meantime; `--force-with-lease` fails loudly instead of
104
+ silently clobbering a concurrent change. If you ever see that step fail
105
+ with "stale info," it means exactly that - fetch, look at what's there,
106
+ and reconcile before retrying.
107
+ - Because the deploy step pushes the whole repo tree (`HEAD:main`), not a
108
+ curated subset, everything here - including this file, the tests, and
109
+ the CI config - ends up on the Space too. There's nothing GitHub-only
110
+ about this process.
111
+
112
+ ## Secrets to know about
113
+
114
+ These expire or rotate silently and are the most likely source of a
115
+ deploy failure that has nothing to do with the code:
116
+
117
+ - `HF_TOKEN` (repo secret) - used to push to the Hugging Face Space.
118
+ - `OPS_CONSUMER_KEY` / `OPS_CONSUMER_SECRET` (optional, set on the Space) -
119
+ EPO OPS API credentials for the patent-search fallback.
120
+ - `SERPENT_API_KEY` (optional, set on the Space) - if set, gates every
121
+ request behind an API key; if you set this and then can't reach the
122
+ deployed API, check here first.
README.md CHANGED
@@ -26,3 +26,6 @@ The suite mocks all outbound HTTP (via `respx`) and drives the Playwright-based
26
  scrapers against local fixture HTML instead of live search engines - see
27
  `tests/helpers.py` for how - so it runs fully offline and doesn't depend on,
28
  or risk tripping, any site's anti-bot measures.
 
 
 
 
26
  scrapers against local fixture HTML instead of live search engines - see
27
  `tests/helpers.py` for how - so it runs fully offline and doesn't depend on,
28
  or risk tripping, any site's anti-bot measures.
29
+
30
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the development methodology
31
+ (TDD workflow, dependency pinning policy, CI/deployment) behind this project.
requirements-dev.txt CHANGED
@@ -1,7 +1,12 @@
1
  # Test-only dependencies. Install with:
2
  # pip install -r requirements.txt -r requirements-dev.txt
3
  # playwright install chromium # if not already present on the machine
 
 
 
 
 
4
  -r requirements.txt
5
  pytest>=8,<10
6
- pytest-asyncio>=0.24
7
- respx>=0.21
 
1
  # Test-only dependencies. Install with:
2
  # pip install -r requirements.txt -r requirements-dev.txt
3
  # playwright install chromium # if not already present on the machine
4
+ #
5
+ # See the pinning note at the top of requirements.txt - same policy here.
6
+ # pytest-asyncio is pinned tighter (patch-only): its event-loop-scope
7
+ # semantics have changed in ways that mattered to us before (see
8
+ # tests/conftest.py's comment on the `browser` fixture).
9
  -r requirements.txt
10
  pytest>=8,<10
11
+ pytest-asyncio>=1.4.0,<1.5
12
+ respx>=0.23.1,<0.24
requirements.txt CHANGED
@@ -1,10 +1,24 @@
1
- fastapi
2
- uvicorn
3
- pydantic
4
- playwright
5
- duckduckgo_search
6
- beautifulsoup4
7
- httpx
8
- lxml
9
- python-dotenv
10
- fastmcp>=3.0,<4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pinned to the versions this codebase is actually tested against (see
2
+ # CONTRIBUTING.md). Range = "current version, up to the next breaking
3
+ # boundary" - the next major for a stable (>=1.0) package, the next minor
4
+ # for a 0.x package (0.x minor bumps are semver's equivalent of a major).
5
+ #
6
+ # playwright and duckduckgo_search are pinned tighter (patch-only): playwright
7
+ # ships in lockstep with a specific browser revision (a version mismatch is a
8
+ # real, previously-hit failure mode - see tests/conftest.py's fallback
9
+ # executable_path logic), and duckduckgo_search has a history of breaking
10
+ # changes between versions.
11
+ #
12
+ # To bump a dependency: update the pin here, run `pytest`, and if you touch
13
+ # playwright specifically, re-run the full suite including
14
+ # tests/test_serp_playwright.py (it drives a real browser).
15
+ fastapi>=0.141.1,<0.142
16
+ uvicorn>=0.52.4,<0.53
17
+ pydantic>=2.13.5,<3
18
+ playwright>=1.62.0,<1.63
19
+ duckduckgo_search>=8.1.1,<8.2
20
+ beautifulsoup4>=4.15.0,<5
21
+ httpx>=0.28.1,<0.29
22
+ lxml>=6.1.2,<7
23
+ python-dotenv>=1.2.3,<2
24
+ fastmcp>=3.4.7,<4