admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / backend / app / scraper / portals.py
5559 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Canonical registry of UK public and private sector Bid Portals checked by Bid Sentinel. Each portal maps to a ``fetch`` coroutine with the signature:: async def fetch(keywords: list[str]) -> list[TenderRecord] Every portal ships with an active scraper (``live=True``). Mechanism per portal, **JSON/OCDS API preferred over HTML**: * Find a Tender and Contracts Finder use the official OCDS JSON APIs — robust. * Any other portal that exposes an OCDS feed can use ``ocds_api.make_scraper( name, api_url)`` — just set its endpoint here. * Portals with no JSON API use the accurate HTML scraper (``generic_html``), which parses structured result cards (title/buyer/value/dates). Because each site's markup differs (and several require an authenticated session), the ``search_url`` / selectors may need per-site tuning before they return results reliably. Switch any of these to ``ocds_api`` the moment a JSON endpoint is confirmed. """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Awaitable, Callable from app.scraper import contracts_finder, find_a_tender, generic_html, ocds_api # noqa: F401 from app.scraper.base import TenderRecord logger = logging.getLogger("scraper.portals") FetchFn = Callable[[list[str]], Awaitable[list[TenderRecord]]] @dataclass(frozen=True) class PortalDef: key: str name: str url: str scope: str fetch: FetchFn live: bool = False # Order here is the display order in the UI. PORTALS: list[PortalDef] = [ PortalDef( key="find_a_tender", name="Find a Tender Service (FTS)", url="https://www.find-tender.service.gov.uk/", scope="Central digital platform for all above-threshold UK public sector procurement.", fetch=find_a_tender.fetch, live=True, ), PortalDef( key="contracts_finder", name="Contracts Finder", url="https://www.contractsfinder.service.gov.uk/Search/Results", scope="Mandatory portal for below-threshold contracts in England and wider public sector.", fetch=contracts_finder.fetch, live=True, ), PortalDef( key="sell2wales", name="Sell2Wales", url="https://www.sell2wales.gov.wales/", scope="Primary procurement portal for all Welsh public bodies.", fetch=generic_html.make_scraper( "Sell2Wales", "https://www.sell2wales.gov.wales/search/Search.aspx" ), live=True, ), PortalDef( key="public_contracts_scotland", name="Public Contracts Scotland (PCS)", url="https://www.publiccontractsscotland.gov.uk/", scope="Primary source for all Scottish public sector tenders.", fetch=generic_html.make_scraper( "Public Contracts Scotland", "https://www.publiccontractsscotland.gov.uk/search/Search.aspx" ), live=True, ), PortalDef( key="etendersni", name="eTendersNI", url="https://etendersni.gov.uk/", scope="Primary eProcurement platform for Northern Ireland.", fetch=generic_html.make_scraper("eTendersNI", "https://etendersni.gov.uk/epps/quickSearchAction.do"), live=True, ), PortalDef( key="procontract", name="ProContract (Due North)", url="https://procontract.due-north.com/", scope="Used by regional councils and local authorities across the UK.", fetch=generic_html.make_scraper( "ProContract (Due North)", "https://procontract.due-north.com/Opportunities/Index" ), live=True, ), PortalDef( key="delta_esourcing", name="Delta eSourcing", url="https://www.delta-esourcing.com/", scope="Common platform used by various contracting authorities to manage bids.", fetch=generic_html.make_scraper( "Delta eSourcing", "https://www.delta-esourcing.com/tenders/UK-UK-Opportunities" ), live=True, ), PortalDef( key="in_tend", name="In-Tend", url="https://in-tendhost.co.uk/", scope="Widely used across the public sector via localized portal instances.", fetch=generic_html.make_scraper("In-Tend", "https://in-tendhost.co.uk/"), live=True, ), PortalDef( key="achilles", name="Achilles", url="https://www.achilles.com/", scope="Supply chain risk/procurement network for utilities, transport and infrastructure.", fetch=generic_html.make_scraper("Achilles", "https://www.achilles.com/find-tenders/"), live=True, ), PortalDef( key="ccs_gca", name="Crown Commercial Service / Government Commercial Agency", url="https://www.gca.gov.uk/", scope="Largest UK public procurement organization; major central government frameworks.", fetch=generic_html.make_scraper("CCS / Government Commercial Agency", "https://www.gca.gov.uk/"), live=True, ), PortalDef( key="digital_marketplace", name="Digital Marketplace (G-Cloud)", url="https://www.applytosupply.digitalmarketplace.service.gov.uk/", scope="Cloud and digital services frameworks (G-Cloud / Digital Outcomes).", fetch=generic_html.make_scraper( "Digital Marketplace (G-Cloud)", "https://www.applytosupply.digitalmarketplace.service.gov.uk/g-cloud/search", ), live=True, ), ] PORTALS_BY_KEY: dict[str, PortalDef] = {p.key: p for p in PORTALS} |