Spanish public-tender alerts for one niche, fire-protection installers and maintainers (protección contra incendios, PCI), built from the open data of the Plataforma de Contratación del Sector Público (PLACSP).
Status: archived experiment. This started as a side-business idea, was built as a working MVP and tested on real data, and was then dropped after a market check: free official alerts and inexpensive commercial services already cover the need. The case study explains what was built, what the data showed and why it stopped. The code stays as a reference for parsing and scoring Spanish procurement data.
- Ingest the two PLACSP syndication feeds (state platform and aggregated regional platforms): live ATOM pages since the last run, or monthly ZIP archives for backfill. TLS is always verified.
- Parse CODICE XML entries into typed
Tenderrecords: CPV codes from the project and every lot, budget, NUTS region, submission deadline, documents and status. - Score relevance deterministically, with the reasons recorded: a core fire-protection CPV is high; related CPVs, or fire keywords in a building/installation context, are medium; firefighting-service supplies (vehicles, boots, uniforms) and forest work are excluded.
- Store the latest snapshot, status history, run cursors and per-profile deliveries in SQLite.
- Digest per profile (CPVs, regions, minimum budget, statuses) as HTML, Markdown or CSV, with sections for new tenders, status changes, tenders closing within 7 days and, optionally, recent awards.
Real output from 2026-10-20 (73,030 tenders ingested): Madrid (Markdown), Madrid (HTML), Andalucía.
- Standard library only (Python 3.12):
xml.etree,sqlite3,ssl,urllib,decimal,tomllib. - Explainable scoring instead of a plain CPV filter: about as many relevant
tenders hide inside generic "mantenimiento integral" contracts as carry the
specific CPV. Every match lists its reasons (
cpv:,keyword:,context:,penalty:). - Real-data fixtures: tests run offline on 21 real feed entries (contact data redacted), including hard cases such as firefighter PPE (CPV 35113xxx), which a naive 4-digit prefix would misclassify.
- Defensive ingestion: bad entries are logged and skipped, pagination is bounded, and the cursor only advances when the previous run is reached.
- Data findings about the PLACSP feeds (formats, volumes, pitfalls) are in docs/data-sources.md.
# Backfill a month (the state archive is about 300 MB)
python -m tenderwatch.cli backfill --month 2026-09 --source all --db data/state.sqlite
# Incremental sync of the live feeds
python -m tenderwatch.cli sync --source all --db data/state.sqlite
# Digest for one profile
python -m tenderwatch.cli digest --db data/state.sqlite \
--profile profiles/example.toml --format html --out out/madrid.html
python -m tenderwatch.cli stats --db data/state.sqliteExit codes: 0 success, 1 runtime or I/O failure, 2 usage or profile
error.
The PLACSP server certificate chains to the Spanish FNMT root ("AC RAIZ
FNMT-RCM SERVIDORES SEGUROS"). Ubuntu trusts it; macOS often does not. Install
it or pass it with --ca-file; never disable verification.
Profile example (profiles/example.toml):
name = "PCI Madrid"
cpv_prefixes = [] # empty = use the fire-protection scorer
nuts_prefixes = ["ES30"] # ES30 Madrid, ES61 Andalucía, ES51 Cataluña...
min_budget = 1000
statuses = ["PUB", "EV"]
min_level = "medium" # high | medium | low
include_awards = truePYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s testsLayout: tenderwatch/{models,parse,cpv,scoring,fetch,state,profiles,digest,cli}.py.
Code under the MIT license. Tender data comes from the open-data syndication of the Plataforma de Contratación del Sector Público (Spanish Ministry of Finance), reused under Spain's public-sector information reuse rules. Sample digests and fixtures are illustrative extracts; the official notices are authoritative.