SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
5.3 KB · 91 lines typescript
Raw Blame History
1import type { Parser, ExtractedRecord, RawDocument } from "@dci/connectors";2import { countryFromText, slugify } from "@dci/core";3import { loadHtml, txt, parenthetical, resolvePlace, regionRecord, type Place } from "./shared.js";4import { normKey } from "./city-coords.js";56/**7 * Oracle Cloud Infrastructure — https://www.oracle.com/cloud/public-cloud-regions/8 * Server-rendered HTML (needs a browser identity: the bot UA gets 403). Sections (h2): Commercial,9 * Sovereign (EU Sovereign Cloud), Government, Multicloud (Oracle Database@AWS/Azure/Google — NOT Oracle10 * regions, skipped). Each table: <td geo-data="us-ashburn-1">US East (Ashburn)</td> … <td>Live|Coming soon|Planned</td>.11 * Quirks seen live: a few rows lack geo-data, one code is duplicated ("Spain Central 2"), and two rows have12 * swapped codes (ap-batam-1 / ap-hyderabad-1). The parser trusts the display name and falls back to a slug13 * code when the geo-data code contradicts the city in the name.14 */15const PREFIX_ISO: Array<[RegExp, string]> = [16  [/^(US|United States)\b/i, "US"], [/^Canada\b/i, "CA"], [/^Mexico\b/i, "MX"], [/^Brazil\b/i, "BR"], [/^Chile\b/i, "CL"], [/^Colombia\b/i, "CO"],17  [/^France\b/i, "FR"], [/^Germany\b/i, "DE"], [/^Italy\b/i, "IT"], [/^Netherlands\b/i, "NL"], [/^Serbia\b/i, "RS"], [/^Spain\b/i, "ES"], [/^Sweden\b/i, "SE"], [/^Switzerland\b/i, "CH"], [/^UK\b/i, "GB"],18  [/^Israel\b/i, "IL"], [/^Kenya\b/i, "KE"], [/^Morocco\b/i, "MA"], [/^Saudi Arabia\b/i, "SA"], [/^South Africa\b/i, "ZA"], [/^UAE\b/i, "AE"],19  [/^Australia/i, "AU"], [/^India\b/i, "IN"], [/^Indonesia\b/i, "ID"], [/^Japan\b/i, "JP"], [/^Malaysia\b/i, "MY"], [/^Singapore\b/i, "SG"], [/^South Korea\b/i, "KR"],20];2122/** OCI codes legitimately named after a different town than the one displayed (uk-cardiff-1 is "UK West (Newport)"). */23const CODE_CITY_ALIASES: Record<string, string[]> = { "uk-cardiff-1": ["newp"], "sa-vinhedo-1": ["vinh"], "ap-chuncheon-1": ["chun"] };2425function statusOf(s: string): "operational" | "announced" {26  return /^live$/i.test(s.trim()) ? "operational" : "announced";27}2829export const oracleRegionsParser: Parser = {30  name: "cloud_oracle_regions_v1",31  version: "1.0.0",32  pageTypes: ["cloud_region"],33  parse(doc: RawDocument, ctx): ExtractedRecord[] {34    const $ = loadHtml(doc);35    const out: ExtractedRecord[] = [];36    const seenCodes = new Set<string>();37    // Walk section headers (h2 inside the accordion list) → tables that follow within the same <li>38    $("li").each((_, li) => {39      const section = txt($(li).find("h2").first());40      if (!/^(Commercial|Sovereign|Government)$/i.test(section)) return;41      const realm = section.toLowerCase();42      $(li).find("table").each((__, table) => {43        const area = txt($(table).find("thead th").first());44        $(table).find("tbody tr").each((___, tr) => {45          const tds = $(tr).find("td");46          if (tds.length < 2) return;47          const first = tds.first();48          const name = txt(first);49          if (!name || /^Oracle (AI )?Database@/i.test(name)) return;50          const statusCell = tds.toArray().map((c) => txt($(c))).find((t) => /^(Live|Coming soon|Planned)$/i.test(t)) ?? "";51          const geoCode = (first.attr("geo-data") ?? "").trim();52          const label = parenthetical(name);53          const iso = PREFIX_ISO.find(([re]) => re.test(name))?.[1] ?? (realm === "sovereign" ? null : countryFromText(name));54          let place: Place = resolvePlace(label, iso);55          if (!place.countryIso2) place.countryIso2 = iso ?? (label ? countryFromText(label) : null) ?? countryFromText(name);56          // Code: trust geo-data unless missing, duplicated, or contradicting the named city (site typo)57          let code = geoCode;58          const cityToken = label ? normKey(label).replace(/\s+/g, "").slice(0, 4) : null;59          const codeBody = code.replace(/-/g, "");60          const contradicts = Boolean(code && cityToken && !codeBody.includes(cityToken) && !(CODE_CITY_ALIASES[code] ?? []).includes(cityToken));61          if (!code || seenCodes.has(code) || contradicts) code = slugify(name);62          seenCodes.add(code);63          const externalIds: Record<string, string | number> = { oci_realm: realm, oci_area: area };64          if (geoCode) externalIds.oci_geo_data = geoCode;65          if (label) externalIds.published_location = label;66          const status = statusOf(statusCell);67          out.push(68            regionRecord({69              connectorId: ctx.connectorId,70              keyPrefix: "oracle:region",71              providerName: "Oracle Cloud",72              code,73              name,74              place,75              availabilityZones: null,76              status: realm === "government" && status === "operational" ? "restricted" : status,77              isSovereign: realm === "sovereign",78              externalIds,79              url: doc.finalUrl,80              methods: { code: geoCode && code === geoCode ? "html:td@geo-data" : "slug:name", name: "html:td", status: "html:td.Status", city: "curated-city-table" },81              certainty: code === geoCode ? 0.95 : 0.85,82            }),83          );84        });85      });86    });87    ctx.log("info", `oracle: ${out.length} regions (${out.filter((r) => r.data.status === "announced").length} announced, ${out.filter((r) => r.data.isSovereign).length} sovereign)`);88    return out;89  },90};91