Spaces:
Sleeping
Sleeping
Safer concurrent deploys, pinned dependencies, and a CONTRIBUTING guide (#5)
Browse files- .github/workflows/deploy-to-hf.yml +7 -1
- .github/workflows/test.yml +33 -0
- CONTRIBUTING.md +122 -0
- README.md +3 -0
- requirements-dev.txt +7 -2
- requirements.txt +24 -10
.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 7 |
-
respx>=0.
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|