SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
20.9 KB · 204 lines python
Raw Blame History
1#!/usr/bin/env python32"""Generate config/sources.d/30-edgar-filings.yaml — SEC EDGAR submissions sensors (`edgar` connector) for a3list of tickers. For each ticker the script fetches the submissions JSON once (name, website, SIC, exchanges),4then either extends an existing source (matched by website apex domain, then by normalized name) or creates a5new organization entry with the company's own website as domain.67Usage:8  python3 scripts/gen-edgar.py fetch  TICKER…            # → /tmp/edgar_meta.json (cached, polite: ≤ 6 req/s)9  python3 scripts/gen-edgar.py emit   > config/sources.d/30-edgar-filings.yaml10Requires: `pip install pyyaml requests` (system python has both here).11"""12import glob13import json14import re15import sys16import time17from urllib.parse import urlparse1819import requests20import yaml2122UA = "WebSensor registry generator (contact@websensor.io)"23META = "/tmp/edgar_meta.json"24TICKERS = "/tmp/tickers.json"  # https://www.sec.gov/files/company_tickers.json25MULTI = {"co.uk", "com.au", "co.jp", "com.br", "co.il", "com.mx", "co.in", "com.tw", "co.kr"}26STOP = {"inc", "corp", "corporation", "co", "company", "ltd", "limited", "plc", "holdings", "holding", "group", "the", "nv", "sa", "se", "ag", "llc", "lp", "de", "class", "a", "b", "com", "trust", "n", "v", "&"}272829def apex(host: str) -> str:30    parts = host.lower().strip(".").split(".")31    if len(parts) >= 3 and ".".join(parts[-2:]) in MULTI:32        return ".".join(parts[-3:])33    return ".".join(parts[-2:])343536def norm(name: str) -> str:37    words = re.sub(r"[^a-z0-9 ]+", " ", name.lower().replace("'", "")).split()38    return " ".join(w for w in words if w not in STOP)394041def slug(name: str) -> str:42    s = re.sub(r"[^a-z0-9]+", "-", norm(name)).strip("-")43    return s or re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")444546def fetch(tickers: list[str]) -> None:47    try:48        meta = json.load(open(META))49    except FileNotFoundError:50        meta = {}51    tk = {v["ticker"]: v for v in json.load(open(TICKERS)).values()}52    for t in tickers:53        if t in meta or t not in tk:54            continue55        cik = f"{int(tk[t]['cik_str']):010d}"56        r = requests.get(f"https://data.sec.gov/submissions/CIK{cik}.json", headers={"User-Agent": UA, "Accept": "application/json"}, timeout=30)57        if r.status_code != 200:58            print(f"{t}: HTTP {r.status_code}", file=sys.stderr)59            continue60        j = r.json()61        meta[t] = {"cik": cik, "name": j.get("name"), "website": j.get("website") or j.get("investorWebsite") or "", "sic": j.get("sicDescription"), "exchanges": j.get("exchanges"), "tickers": j.get("tickers"), "state": j.get("stateOfIncorporation"), "recent": len(((j.get("filings") or {}).get("recent") or {}).get("form", []))}62        json.dump(meta, open(META, "w"), indent=1)63        time.sleep(0.2)64    print(f"{len(meta)} companies in {META}", file=sys.stderr)656667def load_ids() -> tuple[dict[str, str], dict[str, str]]:68    """id → domain, normalized name → id (founding file + fragments < 30)."""69    by_domain: dict[str, str] = {}70    by_name: dict[str, str] = {}71    files = ["config/sources.yaml"] + sorted(f for f in glob.glob("config/sources.d/*.yaml") if int(f.split("/")[-1][:2]) < 30)72    for f in files:73        d = yaml.safe_load(open(f)) or {}74        for s in d.get("sources", []):75            if s.get("extend"):76                continue77            by_domain.setdefault(apex(s["domain"]), s["id"])78            by_name.setdefault(norm(s["name"]), s["id"])79    return by_domain, by_name808182# Ticker → website domain when EDGAR has none (submissions JSON rarely carries `website`).83DOMAINS = {"MMM": "3m.com", "ANF": "abercrombie.com", "AAP": "advanceautoparts.com", "AEO": "ae.com", "AXP": "americanexpress.com", "AMT": "americantower.com", "AMAT": "appliedmaterials.com", "ABG": "asburyauto.com", "ASML": "asml.com", "AN": "autonation.com", "AZO": "autozone.com", "ACN": "accenture.com", "APD": "airproducts.com", "ABNB": "airbnb.com", "ACI": "albertsonscompanies.com", "AA": "alcoa.com", "ADSK": "autodesk.com", "BALL": "ball.com", "BKR": "bakerhughes.com", "BBWI": "bbwinc.com", "BKNG": "bookingholdings.com", "BURL": "burlington.com", "KMX": "carmax.com", "CVNA": "carvana.com", "CAT": "caterpillar.com", "CBRE": "cbre.com", "CHTR": "charter.com", "CHD": "churchdwight.com", "CLF": "clevelandcliffs.com", "CLX": "thecloroxcompany.com", "CNH": "cnh.com", "KO": "coca-colacompany.com", "CL": "colgatepalmolive.com", "COP": "conocophillips.com", "ED": "conedison.com", "COST": "costco.com", "CCI": "crowncastle.com", "CCK": "crowncork.com", "CPRI": "capriholdings.com", "CARG": "cargurus.com", "LNG": "cheniere.com", "DE": "deere.com", "DG": "dollargeneral.com", "DLTR": "dollartree.com", "DOV": "dovercorporation.com", "DOW": "dow.com", "DASH": "doordash.com", "DD": "dupont.com", "ECL": "ecolab.com", "EMR": "emerson.com", "EOG": "eogresources.com", "EL": "elcompanies.com", "ETN": "eaton.com", "ET": "energytransfer.com", "EXPE": "expediagroup.com", "FIVE": "fivebelow.com", "FCX": "fcx.com", "GE": "geaerospace.com", "GIS": "generalmills.com", "GPC": "genpt.com", "GPI": "group1auto.com", "GOLD": "gold.com", "GRAB": "grab.com", "HAL": "halliburton.com", "HSY": "thehersheycompany.com", "HON": "honeywell.com", "HII": "hii.com", "HLT": "hilton.com", "HWM": "howmet.com", "ITW": "itw.com", "IP": "internationalpaper.com", "SJM": "jmsmucker.com", "KMB": "kimberly-clark.com", "KLAC": "kla.com", "KHC": "kraftheinzcompany.com", "LRCX": "lamresearch.com", "LIN": "linde.com", "LAD": "lithiainvestors.com", "LYFT": "lyft.com", "LYB": "lyondellbasell.com", "MAR": "marriott.com", "MCD": "mcdonalds.com", "MPC": "marathonpetroleum.com", "MDLZ": "mondelezinternational.com", "NEM": "newmont.com", "NKE": "nike.com", "NUE": "nucor.com", "ORLY": "oreillyauto.com", "OXY": "oxy.com", "OLLI": "ollies.us", "PKG": "packagingcorp.com", "PAG": "penskeautomotive.com", "PEP": "pepsico.com", "PPG": "ppg.com", "PG": "pg.com", "PLTR": "palantir.com", "PH": "parker.com", "PSX": "phillips66.com", "PSA": "publicstorage.com", "RL": "ralphlauren.com", "O": "realtyincome.com", "REGN": "regeneron.com", "RH": "rh.com", "ROK": "rockwellautomation.com", "ROST": "rossstores.com", "RTX": "rtx.com", "SRE": "sempra.com", "SHW": "sherwin-williams.com", "SPG": "simon.com", "SLB": "slb.com", "SAH": "sonicautomotive.com", "SBUX": "starbucks.com", "STLD": "steeldynamics.com", "TPR": "tapestry.com", "TXN": "ti.com", "TJX": "tjx.com", "TSCO": "tractorsupply.com", "TDG": "transdigm.com", "URBN": "urbn.com", "UBER": "uber.com", "ULTA": "ulta.com", "VLO": "valero.com", "VRTX": "vrtx.com", "VICI": "viciproperties.com", "WEC": "wecenergygroup.com", "WELL": "welltower.com", "WSM": "williams-sonomainc.com", "XEL": "xcelenergy.com", "UNH": "unitedhealthgroup.com", "XOM": "exxonmobil.com", "JNJ": "jnj.com", "HD": "homedepot.com", "WMT": "walmart.com", "CVX": "chevron.com", "LOW": "lowes.com", "TGT": "target.com", "KR": "thekrogerco.com", "BBY": "bestbuy.com", "EBAY": "ebayinc.com", "ETSY": "etsy.com", "W": "wayfair.com", "CHWY": "chewy.com", "DAL": "delta.com", "UAL": "united.com", "AAL": "aa.com", "LUV": "southwest.com", "JBLU": "jetblue.com", "ALK": "alaskaair.com", "LMT": "lockheedmartin.com", "NOC": "northropgrumman.com", "GD": "gd.com", "LHX": "l3harris.com", "BA": "boeing.com", "UNP": "up.com", "DUK": "duke-energy.com", "NEE": "nexteraenergy.com", "SO": "southerncompany.com", "D": "dominionenergy.com", "EXC": "exeloncorp.com", "AEP": "aep.com", "KMI": "kindermorgan.com", "WMB": "williams.com", "T": "att.com", "VZ": "verizon.com", "TMUS": "t-mobile.com", "CMCSA": "corporate.comcast.com", "DIS": "thewaltdisneycompany.com", "AVGO": "broadcom.com", "MDT": "medtronic.com", "SYK": "stryker.com", "BSX": "bostonscientific.com", "BDX": "bd.com", "EW": "edwards.com", "DXCM": "dexcom.com", "ISRG": "intuitive.com", "ZBH": "zimmerbiomet.com", "PLD": "prologis.com", "EQIX": "equinix.com", "DLR": "digitalrealty.com", "ABT": "abbott.com", "ABBV": "abbvie.com", "AMGN": "amgen.com", "BMY": "bms.com", "GILD": "gilead.com", "BNTX": "biontech.com", "CVS": "cvshealth.com", "CI": "thecignagroup.com", "HUM": "humana.com", "ELV": "elevancehealth.com", "TMO": "thermofisher.com", "LULU": "lululemon.com", "SNOW": "snowflake.com", "DDOG": "datadoghq.com", "TEAM": "atlassian.com", "WDAY": "workday.com", "INTU": "intuit.com", "ADBE": "adobe.com", "NET": "cloudflare.com", "MDB": "mongodb.com", "ANET": "arista.com", "PANW": "paloaltonetworks.com", "CRWD": "crowdstrike.com", "FTNT": "fortinet.com", "ZS": "zscaler.com", "SPGI": "spglobal.com", "GS": "goldmansachs.com", "MS": "morganstanley.com", "BLK": "blackrock.com", "JPM": "jpmorganchase.com", "BAC": "bankofamerica.com", "WFC": "wellsfargo.com", "C": "citigroup.com", "HOOD": "robinhood.com", "SOFI": "sofi.com", "AFRM": "affirm.com", "CTVA": "corteva.com", "CPB": "thecampbellscompany.com", "CAG": "conagrabrands.com", "MRNA": "modernatx.com", "PFE": "pfizer.com", "MRK": "merck.com", "LLY": "lilly.com", "AGCO": "agcocorp.com", "TSM": "tsmc.com", "ARM": "arm.com", "ORCL": "oracle.com", "IBM": "ibm.com", "NFLX": "netflix.com", "TSLA": "tesla.com", "F": "ford.com", "GM": "gm.com", "RIVN": "rivian.com", "LCID": "lucidmotors.com", "TM": "toyota.com", "HMC": "honda.com", "STLA": "stellantis.com", "V": "visa.com", "MA": "mastercard.com", "PYPL": "paypal.com", "COIN": "coinbase.com", "SHOP": "shopify.com", "CRM": "salesforce.com", "NOW": "servicenow.com", "AMD": "amd.com", "INTC": "intel.com", "QCOM": "qualcomm.com", "MU": "micron.com", "CSCO": "cisco.com", "META": "meta.com", "GOOGL": "abc.xyz", "AMZN": "amazon.com", "AAPL": "apple.com", "MSFT": "microsoft.com", "NVDA": "nvidia.com", "BRK-B": "berkshirehathaway.com", "ASML": "asml.com", "AXP": "americanexpress.com"}8485# Ticker → existing id when domain/name matching is not enough.86MANUAL = {"GOOGL": "google", "META": "meta-ai", "BRK-B": "berkshire-hathaway", "AMZN": "amazon", "TSM": "tsmc", "SQ": "block", "COIN": "coinbase", "SHOP": "shopify", "ARM": "arm", "CRM": "salesforce", "NOW": "servicenow", "MSFT": "microsoft", "AAPL": "apple", "NVDA": "nvidia", "AMD": "amd", "INTC": "intel", "QCOM": "qualcomm", "MU": "micron", "ORCL": "oracle-cloud", "IBM": "ibm-cloud", "NFLX": "netflix", "TSLA": "tesla", "F": "ford", "GM": "gm", "RIVN": "rivian", "LCID": "lucid", "TM": "toyota", "HMC": "honda", "STLA": "stellantis", "PFE": "pfizer", "MRNA": "moderna", "MRK": "merck", "LLY": "eli-lilly", "AZN": "astrazeneca", "NVS": "novartis", "GSK": "gsk", "SNY": "sanofi", "V": "visa", "MA": "mastercard", "PYPL": "paypal", "UBER": "uber", "ANET": "arista", "PANW": "palo-alto-networks", "CRWD": "crowdstrike", "FTNT": "fortinet", "NET": "cloudflare", "MDB": "mongodb", "TEAM": "atlassian", "DELL": "dell", "HPQ": "hp", "WDC": "western-digital", "STX": "seagate", "HOOD": "robinhood", "SNOW": "snowflake", "DDOG": "datadog", "ZS": "zscaler", "WDAY": "workday", "ADSK": "autodesk", "ADBE": "adobe", "INTU": "intuit", "CSCO": "cisco-security", "DIS": "disney", "CMCSA": "comcast", "T": "att", "VZ": "verizon", "TMUS": "t-mobile", "SPGI": "sp-global", "BLK": "blackrock", "GS": "goldman-sachs", "MS": "morgan-stanley", "JPM": "jpmorgan", "BAC": "bank-of-america", "WFC": "wells-fargo", "C": "citi"}878889def emit() -> None:90    meta = json.load(open(META))91    by_domain, by_name = load_ids()92    all_ids = set(by_domain.values()) | set(by_name.values())93    print("# WebSensor — SEC EDGAR filings sensor class (generated 2026-09-08 by scripts/gen-edgar.py from")94    print("# data.sec.gov submissions; validated live). One `edgar` sensor per issuer: 8-K/10-K/10-Q/S-1/6-K/13D/DEF 14A…")95    print("# (insider forms 3/4/5 and 144 excluded). Existing organizations are extended; the others are created with")96    print("# their own website as domain. Tier B for mega-caps, C otherwise.")97    print("sources:")98    used: set[str] = set()99    n_ext = n_new = 0100    for t, m in sorted(meta.items(), key=lambda kv: kv[1]["name"]):101        host = urlparse(m["website"] if m["website"].startswith("http") else "https://" + m["website"]).hostname if m["website"] else None102        ap = apex(host) if host else None103        target = MANUAL.get(t) if MANUAL.get(t) in all_ids else None104        if not target and ap and ap in by_domain:105            target = by_domain[ap]106        if not target and norm(m["name"]) in by_name:107            target = by_name[norm(m["name"])]108        if not target:109            # prefix match on normalized names ("costco wholesale" ↔ "costco"), ≥ 5 chars, unambiguous110            nm = norm(m["name"])111            cands = [sid for n, sid in by_name.items() if len(n) >= 5 and (nm.startswith(n + " ") or nm == n or n.startswith(nm + " "))]112            if len(set(cands)) == 1:113                target = cands[0]114        if not host and t in DOMAINS:115            host = DOMAINS[t]116            ap = apex(host)117            if not target and ap in by_domain:118                target = by_domain[ap]119        sensor = f'      - {{ name: edgar filings, url: "https://data.sec.gov/submissions/CIK{m["cik"]}.json", type: REST_API, connector: edgar, tier: {"B" if t in MEGA else "C"} }}'120        if target:121            if target in used:122                continue123            used.add(target)124            print(f"  - id: {target}")125            print("    extend: true")126            print("    categories: [filings]")127            print(f"    aliases: [{t.lower()}]")128            print("    sensors:")129            print(sensor)130            n_ext += 1131            continue132        if not ap:133            print(f"# {t} ({m['name']}): no website in EDGAR and no DOMAINS entry — skipped", file=sys.stderr)134            continue135        sid = slug(m["name"])136        if sid in all_ids or sid in used:137            sid = f"{sid}-{t.lower()}"138        used.add(sid)139        name = pretty_name(t, m["name"])140        print(f"  - id: {sid}")141        print(f"    name: {json.dumps(name, ensure_ascii=False)}")142        print(f"    domain: {ap}")143        print(f"    homepage: https://{'www.' + host if host.count('.') == 1 else host}")144        print(f"    categories: [filings, {sector(m.get('sic') or '')}]")145        print(f"    tier: {'B' if t in MEGA else 'C'}")146        print(f"    aliases: [{t.lower()}]")147        print(f"    notes: \"SIC: {m.get('sic') or 'n/a'} · exchanges: {', '.join(m.get('exchanges') or [])} · CIK {int(m['cik'])}\"")148        print("    discover: { rss: true }")149        print("    sensors:")150        print(sensor)151        n_new += 1152    print(f"{n_ext} extended, {n_new} new", file=sys.stderr)153154155MEGA = set("AAPL MSFT GOOGL AMZN NVDA META TSLA BRK-B JPM V MA UNH XOM JNJ PG HD COST ABBV MRK CVX KO PEP WMT BAC CRM ORCL AMD INTC NFLX ADBE CSCO AVGO LLY TSM ASML".split())156157158NAMES = {"MMM": "3M", "ANF": "Abercrombie & Fitch", "AAP": "Advance Auto Parts", "AEO": "American Eagle Outfitters", "AXP": "American Express", "AMT": "American Tower", "AMAT": "Applied Materials", "ABG": "Asbury Automotive Group", "ASML": "ASML", "AN": "AutoNation", "AZO": "AutoZone", "ACN": "Accenture", "APD": "Air Products", "ACI": "Albertsons Companies", "ADSK": "Autodesk", "BALL": "Ball Corporation", "BKR": "Baker Hughes", "BBWI": "Bath & Body Works", "BKNG": "Booking Holdings", "BURL": "Burlington Stores", "KMX": "CarMax", "CVNA": "Carvana", "CAT": "Caterpillar", "CBRE": "CBRE Group", "CHTR": "Charter Communications", "CHD": "Church & Dwight", "CLF": "Cleveland-Cliffs", "CLX": "Clorox", "CNH": "CNH Industrial", "KO": "The Coca-Cola Company", "CL": "Colgate-Palmolive", "COP": "ConocoPhillips", "ED": "Consolidated Edison", "COST": "Costco Wholesale", "CCI": "Crown Castle", "CCK": "Crown Holdings", "CPRI": "Capri Holdings", "CARG": "CarGurus", "LNG": "Cheniere Energy", "DE": "Deere & Company", "DG": "Dollar General", "DLTR": "Dollar Tree", "DOV": "Dover Corporation", "DOW": "Dow", "DASH": "DoorDash", "DD": "DuPont", "ECL": "Ecolab", "EMR": "Emerson Electric", "EOG": "EOG Resources", "EL": "The Estée Lauder Companies", "ETN": "Eaton", "ET": "Energy Transfer", "EXPE": "Expedia Group", "FIVE": "Five Below", "FCX": "Freeport-McMoRan", "GE": "GE Aerospace", "GIS": "General Mills", "GPC": "Genuine Parts Company", "GPI": "Group 1 Automotive", "GOLD": "Gold.com", "GRAB": "Grab Holdings", "HAL": "Halliburton", "HSY": "The Hershey Company", "HON": "Honeywell", "HII": "Huntington Ingalls Industries", "HLT": "Hilton Worldwide", "HWM": "Howmet Aerospace", "ITW": "Illinois Tool Works", "IP": "International Paper", "SJM": "The J.M. Smucker Company", "KMB": "Kimberly-Clark", "KLAC": "KLA Corporation", "KHC": "Kraft Heinz", "LRCX": "Lam Research", "LIN": "Linde", "LAD": "Lithia Motors", "LYFT": "Lyft", "LYB": "LyondellBasell", "MAR": "Marriott International", "MCD": "McDonald's", "MPC": "Marathon Petroleum", "MDLZ": "Mondelez International", "NEM": "Newmont", "NKE": "Nike", "NUE": "Nucor", "ORLY": "O'Reilly Automotive", "OXY": "Occidental Petroleum", "OLLI": "Ollie's Bargain Outlet", "PKG": "Packaging Corporation of America", "PAG": "Penske Automotive Group", "PEP": "PepsiCo", "PPG": "PPG Industries", "PG": "Procter & Gamble", "PLTR": "Palantir Technologies", "PH": "Parker-Hannifin", "PSX": "Phillips 66", "PSA": "Public Storage", "RL": "Ralph Lauren", "O": "Realty Income", "REGN": "Regeneron Pharmaceuticals", "RH": "RH", "ROK": "Rockwell Automation", "ROST": "Ross Stores", "RTX": "RTX", "SRE": "Sempra", "SHW": "Sherwin-Williams", "SPG": "Simon Property Group", "SLB": "SLB", "SAH": "Sonic Automotive", "SBUX": "Starbucks", "STLD": "Steel Dynamics", "TPR": "Tapestry", "TXN": "Texas Instruments", "TJX": "TJX Companies", "TSCO": "Tractor Supply", "TDG": "TransDigm Group", "URBN": "Urban Outfitters", "UBER": "Uber", "ULTA": "Ulta Beauty", "VLO": "Valero Energy", "VRTX": "Vertex Pharmaceuticals", "VICI": "VICI Properties", "WEC": "WEC Energy Group", "WELL": "Welltower", "WSM": "Williams-Sonoma", "XEL": "Xcel Energy", "UNH": "UnitedHealth Group", "XOM": "ExxonMobil", "JNJ": "Johnson & Johnson", "HD": "The Home Depot", "WMT": "Walmart", "CVX": "Chevron", "LOW": "Lowe's", "TGT": "Target", "KR": "Kroger", "BBY": "Best Buy", "EBAY": "eBay", "W": "Wayfair", "CHWY": "Chewy", "DAL": "Delta Air Lines", "UAL": "United Airlines", "AAL": "American Airlines", "LUV": "Southwest Airlines", "JBLU": "JetBlue", "ALK": "Alaska Air Group", "LMT": "Lockheed Martin", "NOC": "Northrop Grumman", "GD": "General Dynamics", "LHX": "L3Harris", "BA": "Boeing", "UNP": "Union Pacific", "DUK": "Duke Energy", "NEE": "NextEra Energy", "SO": "Southern Company", "D": "Dominion Energy", "EXC": "Exelon", "AEP": "American Electric Power", "KMI": "Kinder Morgan", "WMB": "Williams Companies", "T": "AT&T", "VZ": "Verizon", "TMUS": "T-Mobile US", "CMCSA": "Comcast", "DIS": "The Walt Disney Company", "AVGO": "Broadcom", "MDT": "Medtronic", "SYK": "Stryker", "BSX": "Boston Scientific", "BDX": "BD", "EW": "Edwards Lifesciences", "DXCM": "Dexcom", "ISRG": "Intuitive Surgical", "ZBH": "Zimmer Biomet", "PLD": "Prologis", "EQIX": "Equinix", "DLR": "Digital Realty", "ABT": "Abbott", "ABBV": "AbbVie", "AMGN": "Amgen", "BMY": "Bristol Myers Squibb", "GILD": "Gilead Sciences", "BNTX": "BioNTech", "CVS": "CVS Health", "CI": "The Cigna Group", "HUM": "Humana", "ELV": "Elevance Health", "TMO": "Thermo Fisher Scientific", "LULU": "Lululemon", "AGCO": "AGCO Corporation", "BRK-B": "Berkshire Hathaway", "AA": "Alcoa", "ABNB": "Airbnb"}159160161def pretty_name(ticker: str, raw: str) -> str:162    if ticker in NAMES:163        return NAMES[ticker]164    n = re.sub(r"\s*/\s*[A-Z]{2}\s*/?\s*$", "", raw)  # trailing state fragments "/DE/", "/MA/"165    n = re.sub(r"\s*/\s*(NEW|OLD)\s*/?\s*$", "", n, flags=re.I)166    words = []167    for w in n.split():168        lw = w.lower().strip(",.")169        if lw in ("inc", "corp", "co", "ltd", "plc", "nv", "n.v", "llc", "lp", "sa", "se", "ag"):170            words.append({"inc": "Inc.", "corp": "Corp.", "co": "Co.", "ltd": "Ltd.", "plc": "plc", "nv": "N.V.", "n.v": "N.V.", "llc": "LLC", "lp": "LP", "sa": "SA", "se": "SE", "ag": "AG"}[lw])171        elif w.isupper() and len(w) > 3:172            words.append(w.title())173        else:174            words.append(w)175    return re.sub(r"\s+", " ", " ".join(words)).replace(" ,", ",").strip(" ,")176177178def sector(sic: str) -> str:179    s = sic.lower()180    if any(k in s for k in ("pharma", "biolog", "medic", "surgical", "health", "hospital")):181        return "health"182    if any(k in s for k in ("bank", "finance", "insurance", "security", "invest", "loan", "credit")):183        return "finance"184    if any(k in s for k in ("computer", "software", "semiconductor", "electronic", "telephone", "communication", "data processing")):185        return "technology"186    if any(k in s for k in ("petroleum", "oil", "gas", "electric", "energy", "mining", "coal")):187        return "energy"188    if any(k in s for k in ("retail", "store", "restaurant", "apparel", "grocer", "eating")):189        return "retail"190    if any(k in s for k in ("motor", "aircraft", "air transport", "railroad", "truck", "ship")):191        return "transport"192    if any(k in s for k in ("real estate", "reit")):193        return "real-estate"194    if any(k in s for k in ("food", "beverage", "agricultur", "tobacco")):195        return "food"196    return "enterprise"197198199if __name__ == "__main__":200    if sys.argv[1:2] == ["fetch"]:201        fetch(sys.argv[2:])202    else:203        emit()204