Compare commits
10 Commits
011017bde7
...
fa7e3eb545
| Author | SHA1 | Date | |
|---|---|---|---|
| fa7e3eb545 | |||
| 873a6b6717 | |||
| 7fd9517a14 | |||
| 1866cf48ee | |||
| 9d305c869b | |||
| 3046fac4e8 | |||
| fde7f19330 | |||
| 166720071f | |||
| 025bbcb52d | |||
| e3c403c326 |
25
.github/workflows/test.yml
vendored
Normal file
25
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pytest
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest tests/ -v
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
*.log
|
||||
cache/*
|
||||
!cache/.gitkeep
|
||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py .
|
||||
COPY templates/ templates/
|
||||
COPY cache/ cache/
|
||||
EXPOSE 5001
|
||||
CMD ["python", "app.py"]
|
||||
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# 10k Viewer
|
||||
|
||||
Web interface for browsing SEC 10-K filings. Query any publicly traded company by ticker, fetch filings data from SEC EDGAR API, and render structured HTML views.
|
||||
|
||||
## Features
|
||||
|
||||
- Search 10-K filings by company ticker symbol
|
||||
- Built-in fallback CIK database for S&P 500 companies
|
||||
- SEC EDGAR API integration with retry logic
|
||||
- Flask-based web interface with template rendering
|
||||
- Local caching for fast repeat queries
|
||||
- Docker-ready for deployment
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** Python 3.12, Flask
|
||||
- **Frontend:** HTML templates (Jinja2)
|
||||
- **API:** SEC EDGAR REST API
|
||||
- **Deployment:** Docker, port 5001
|
||||
|
||||
## Setup
|
||||
|
||||
### Local
|
||||
|
||||
```bash
|
||||
pip install flask
|
||||
python app.py
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:5001`
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker build -t 10k-viewer .
|
||||
docker run -p 5001:5001 10k-viewer
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Enter a stock ticker (e.g. `AAPL`, `MSFT`, `GOOGL`) to fetch and browse the latest 10-K filing data. Supports any company with a valid CIK identifier.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt pytest
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
233
app.py
Normal file
233
app.py
Normal file
@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal SEC filings viewer."""
|
||||
|
||||
import gzip, json, logging, re, time, urllib.request, urllib.error
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, abort, send_from_directory, request, make_response
|
||||
|
||||
app = Flask(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("10k")
|
||||
|
||||
UA = "10k-viewer/1.0 (Jarian Cottingham jarianc@proton.me)"
|
||||
CACHE_DIR = Path(__file__).parent / "cache"
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
MAX_RETRIES = 1
|
||||
PAGE_SIZE = 10
|
||||
|
||||
FALLBACK = {
|
||||
"AAPL": {"cik": "0000320193", "name": "Apple Inc."},
|
||||
"MSFT": {"cik": "0000789019", "name": "Microsoft Corp"},
|
||||
"GOOGL": {"cik": "0001652044", "name": "Alphabet Inc."},
|
||||
"AMZN": {"cik": "0001018724", "name": "Amazon.com Inc."},
|
||||
"META": {"cik": "0001326801", "name": "Meta Platforms Inc."},
|
||||
"NVDA": {"cik": "0001045810", "name": "NVIDIA Corp"},
|
||||
"TSLA": {"cik": "0001318605", "name": "Tesla Inc."},
|
||||
"JPM": {"cik": "0000019617", "name": "JPMorgan Chase"},
|
||||
"V": {"cik": "0001403161", "name": "Visa Inc."},
|
||||
"JNJ": {"cik": "0000200406", "name": "Johnson & Johnson"},
|
||||
"WMT": {"cik": "0000104169", "name": "Walmart Inc."},
|
||||
"PG": {"cik": "0000080424", "name": "Procter & Gamble"},
|
||||
"MA": {"cik": "0001141391", "name": "Mastercard Inc."},
|
||||
"HD": {"cik": "0000354950", "name": "Home Depot Inc."},
|
||||
"DIS": {"cik": "0001001039", "name": "Walt Disney Co"},
|
||||
"NFLX": {"cik": "0001065280", "name": "Netflix Inc."},
|
||||
"ADBE": {"cik": "0000796343", "name": "Adobe Inc."},
|
||||
"CRM": {"cik": "0001108524", "name": "Salesforce Inc."},
|
||||
"INTC": {"cik": "0000050863", "name": "Intel Corp"},
|
||||
"AMD": {"cik": "0000002488", "name": "Advanced Micro Devices"},
|
||||
"COST": {"cik": "0000909832", "name": "Costco Wholesale"},
|
||||
"KO": {"cik": "0000021344", "name": "Coca-Cola Co"},
|
||||
"AVGO": {"cik": "0001730168", "name": "Broadcom Inc."},
|
||||
"ORCL": {"cik": "0001341439", "name": "Oracle Corp"},
|
||||
"LLY": {"cik": "0000059478", "name": "Eli Lilly & Co"},
|
||||
"CSCO": {"cik": "0000858877", "name": "Cisco Systems"},
|
||||
"MCD": {"cik": "0000063908", "name": "McDonald's Corp"},
|
||||
"NKE": {"cik": "0000320187", "name": "Nike Inc."},
|
||||
"QCOM": {"cik": "0000804328", "name": "Qualcomm Inc."},
|
||||
"TXN": {"cik": "0000010454", "name": "Texas Instruments"},
|
||||
"SBUX": {"cik": "0000829224", "name": "Starbucks Corp"},
|
||||
"LOW": {"cik": "0000300026", "name": "Lowe's Cos Inc."},
|
||||
"PANW": {"cik": "0001742704", "name": "Palo Alto Networks"},
|
||||
"NOW": {"cik": "0001590894", "name": "ServiceNow Inc."},
|
||||
"AMAT": {"cik": "0000002017", "name": "Applied Materials"},
|
||||
"MU": {"cik": "0000727119", "name": "Micron Technology"},
|
||||
"ASML": {"cik": "0001647072", "name": "ASML Holding"},
|
||||
"SNPS": {"cik": "0000002915", "name": "Synopsys Inc."},
|
||||
"CDNS": {"cik": "0000007643", "name": "Cadence Design"},
|
||||
"ADI": {"cik": "0000006281", "name": "Analog Devices"},
|
||||
"FTNT": {"cik": "0001645674", "name": "Fortinet Inc."},
|
||||
"CRWD": {"cik": "0001864275", "name": "CrowdStrike"},
|
||||
"ZS": {"cik": "0001783140", "name": "Zscaler Inc."},
|
||||
"DDOG": {"cik": "0001779937", "name": "Datadog Inc."},
|
||||
"SNOW": {"cik": "0001640185", "name": "Snowflake Inc."},
|
||||
"PLTR": {"cik": "0001781878", "name": "Palantir Tech"},
|
||||
"COIN": {"cik": "0001679788", "name": "Coinbase Global"},
|
||||
"RBLX": {"cik": "0001816155", "name": "Roblox Corp"},
|
||||
"SQ": {"cik": "0001594968", "name": "Block Inc."},
|
||||
"SHOP": {"cik": "0001594805", "name": "Shopify Inc."},
|
||||
"UBER": {"cik": "0001543151", "name": "Uber Technologies"},
|
||||
"DASH": {"cik": "0001715041", "name": "DoorDash Inc."},
|
||||
"ABNB": {"cik": "0001792947", "name": "Airbnb Inc."},
|
||||
"RIVN": {"cik": "0001874178", "name": "Rivian Automotive"},
|
||||
"LCID": {"cik": "0001963077", "name": "Lucid Group"},
|
||||
"NIO": {"cik": "0001841781", "name": "NIO Inc."},
|
||||
"BABA": {"cik": "0001577552", "name": "Alibaba Group"},
|
||||
}
|
||||
|
||||
_tickers = None
|
||||
_filings_cache = {}
|
||||
|
||||
|
||||
def fetch_json(url):
|
||||
for _ in range(1 + MAX_RETRIES):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
d = r.read()
|
||||
if r.headers.get("Content-Encoding") == "gzip":
|
||||
d = gzip.decompress(d)
|
||||
return json.loads(d.decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (403, 404, 429):
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
raise Exception(f"Failed {url}")
|
||||
|
||||
|
||||
def fetch_filing(url):
|
||||
for _ in range(1 + MAX_RETRIES):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read()
|
||||
except Exception as e:
|
||||
log.warning(f"Download failed: {e}")
|
||||
raise Exception(f"Download failed {url}")
|
||||
|
||||
|
||||
def get_tickers():
|
||||
global _tickers
|
||||
if _tickers:
|
||||
return _tickers
|
||||
cf = CACHE_DIR / "tickers.json"
|
||||
if cf.exists():
|
||||
try:
|
||||
_tickers = json.loads(cf.read_text())
|
||||
return _tickers
|
||||
except Exception:
|
||||
pass
|
||||
_tickers = FALLBACK
|
||||
return _tickers
|
||||
|
||||
|
||||
def parse_filings(subs, cik, forms):
|
||||
recent = subs.get("filings", {}).get("recent", {})
|
||||
fs = recent.get("form", [])
|
||||
ds = recent.get("filingDate", [])
|
||||
doc = recent.get("primaryDocument", [])
|
||||
acc = recent.get("accessionNumber", [])
|
||||
r = []
|
||||
for i, f in enumerate(fs):
|
||||
if f not in forms:
|
||||
continue
|
||||
a = acc[i].replace("-", "")
|
||||
r.append({
|
||||
"form": f,
|
||||
"date": ds[i] if i < len(ds) else "?",
|
||||
"file": f"{ds[i]}_{a}.html" if i < len(ds) else f"?_{a}.html",
|
||||
"url": f"https://www.sec.gov/Archives/edgar/data/{cik}/{a}/{doc[i]}",
|
||||
})
|
||||
return r
|
||||
|
||||
|
||||
def get_filings(cik, forms):
|
||||
key = f"{cik}:{','.join(sorted(forms))}"
|
||||
if key in _filings_cache:
|
||||
return _filings_cache[key]
|
||||
subs = fetch_json(f"https://data.sec.gov/submissions/CIK{cik}.json")
|
||||
r = parse_filings(subs, cik, forms)
|
||||
_filings_cache[key] = r
|
||||
return r
|
||||
|
||||
|
||||
def clean_ereader(html):
|
||||
t = html.decode("utf-8", errors="replace")
|
||||
t = re.sub(r'<script[^>]*>.*?</script>', '', t, flags=re.DOTALL|re.I)
|
||||
t = re.sub(r'<style[^>]*>.*?</style>', '', t, flags=re.DOTALL|re.I)
|
||||
t = re.sub(r"""\s+style=["'][^"']*["']""", '', t, flags=re.I)
|
||||
t = re.sub(r"""\s+on\w+=["'][^"']*["']""", '', t, flags=re.I)
|
||||
t = re.sub(r"""\s+(class|id)=["'][^"']*["']""", '', t, flags=re.I)
|
||||
t = re.sub(r'<!--.*?-->', '', t, flags=re.DOTALL)
|
||||
s = '<style>body{font-family:Georgia,serif!important;font-size:18px!important;line-height:1.6!important;max-width:700px;margin:0 auto;padding:20px;color:#000;background:#fff}table{border-collapse:collapse;width:100%;margin:10px 0;font-size:14px}th,td{border:1px solid #000;padding:4px 8px;text-align:left}th{font-weight:bold;background:#eee}a{color:#000}h1,h2,h3,h4,h5,h6{font-family:Georgia,serif!important;margin:16px 0 8px}p{margin:8px 0}</style>'
|
||||
if '<head>' in t.lower():
|
||||
t = t.replace('<head>', f'<head>{s}', 1)
|
||||
elif '<body' in t.lower():
|
||||
t = t.replace('<body', f'<body{s}', 1)
|
||||
else:
|
||||
t = s + t
|
||||
return t.encode("utf-8")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
q = request.args.get("q", "").strip()
|
||||
t = get_tickers()
|
||||
if q:
|
||||
q2 = q.upper()
|
||||
c = {k: v for k, v in t.items() if q2 in k.upper() or q2 in v["name"].upper()}
|
||||
else:
|
||||
c = dict(list(t.items())[:50])
|
||||
q = ""
|
||||
return render_template("index.html", companies=c, query=q, total=len(c))
|
||||
|
||||
|
||||
@app.route("/<ticker>")
|
||||
def company(ticker):
|
||||
t = get_tickers()
|
||||
if ticker not in t:
|
||||
abort(404)
|
||||
info = t[ticker]
|
||||
ff = request.args.get("form", "").upper()
|
||||
forms = {"10-K"} if ff == "10-K" else {"10-Q"} if ff == "10-Q" else {"10-K", "10-Q"}
|
||||
try:
|
||||
all_f = get_filings(info["cik"], forms)
|
||||
except Exception as e:
|
||||
log.error(f"Failed {ticker}: {e}")
|
||||
abort(503, description=str(e))
|
||||
p = request.args.get("page", 1, type=int)
|
||||
s = (p - 1) * PAGE_SIZE
|
||||
e = s + PAGE_SIZE
|
||||
page = all_f[s:e]
|
||||
tp = (len(all_f) + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
return render_template("company.html", ticker=ticker, company=info, cik=info["cik"],
|
||||
filings=page, form_filter=ff, page=p, total_pages=tp, total=len(all_f))
|
||||
|
||||
|
||||
@app.route("/<ticker>/view/<filename>")
|
||||
def view(ticker, filename):
|
||||
t = get_tickers()
|
||||
if ticker not in t:
|
||||
abort(404)
|
||||
cik = t[ticker]["cik"]
|
||||
dest = CACHE_DIR / "filings" / cik / filename
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not dest.exists():
|
||||
try:
|
||||
fs = get_filings(cik, {"10-K", "10-Q"})
|
||||
url = next((f["url"] for f in fs if f["file"] == filename), None)
|
||||
if not url:
|
||||
abort(404, description="Not found")
|
||||
log.info(f"Caching {filename}...")
|
||||
dest.write_bytes(fetch_filing(url))
|
||||
except Exception as e:
|
||||
log.error(f"Cache fail {ticker}/{filename}: {e}")
|
||||
abort(503, description=str(e))
|
||||
if request.args.get("reader", "").lower() in ("1", "true"):
|
||||
return make_response(clean_ereader(dest.read_bytes())), {"Content-Type": "text/html"}
|
||||
return send_from_directory(str(dest.parent), filename, mimetype="text/html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5001)
|
||||
0
cache/.gitkeep
vendored
Normal file
0
cache/.gitkeep
vendored
Normal file
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
flask>=3.0,<4
|
||||
49
templates/company.html
Normal file
49
templates/company.html
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ ticker }} Filings</title>
|
||||
<style>
|
||||
body{font-family:Georgia,serif;font-size:18px;max-width:600px;margin:0 auto;padding:20px}
|
||||
h1{font-size:1.4em}
|
||||
ul{list-style:none;padding:0}
|
||||
li{padding:8px 0;border-bottom:1px solid #ccc}
|
||||
a{color:#000}
|
||||
small{color:#555}
|
||||
.back{font-size:0.85em}
|
||||
.filter{margin:10px 0}
|
||||
.filter a{margin-right:15px;font-size:0.9em}
|
||||
.filter a.active{font-weight:bold;text-decoration:underline}
|
||||
.ftag{display:inline-block;padding:1px 5px;font-size:0.75em;margin-left:5px}
|
||||
.f10k{background:#000;color:#fff}
|
||||
.f10q{background:#666;color:#fff}
|
||||
.pg{margin:15px 0;font-size:0.9em}
|
||||
.pg a{margin-right:10px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="back"><a href="/">← All companies</a></p>
|
||||
<h1>{{ ticker }} — {{ company.name }}</h1>
|
||||
<div class="filter">
|
||||
<a href="/{{ ticker }}" class="{% if not form_filter %}active{% endif %}">All</a>
|
||||
<a href="/{{ ticker }}?form=10-K" class="{% if form_filter == '10-K' %}active{% endif %}">10-K Annual</a>
|
||||
<a href="/{{ ticker }}?form=10-Q" class="{% if form_filter == '10-Q' %}active{% endif %}">10-Q Quarterly</a>
|
||||
</div>
|
||||
<p>Total: {{ total }} filings · Page {{ page }} of {{ total_pages }}</p>
|
||||
<ul>
|
||||
{% for f in filings %}
|
||||
<li>
|
||||
<a href="/{{ ticker }}/view/{{ f.file }}?reader=1">{{ f.date }}</a>
|
||||
<span class="ftag {% if f.form == '10-K' %}f10k{% else %}f10q{% endif %}">{{ f.form }}</span>
|
||||
<small> · <a href="/{{ ticker }}/view/{{ f.file }}">raw</a></small>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>No filings found.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pg">
|
||||
{% if page > 1 %}<a href="/{{ ticker }}?form={{ form_filter }}&page={{ page - 1 }}">← Prev</a>{% endif %}
|
||||
{% if page < total_pages %}<a href="/{{ ticker }}?form={{ form_filter }}&page={{ page + 1 }}">Next →</a>{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
48
templates/index.html
Normal file
48
templates/index.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>10-K Viewer</title>
|
||||
<style>
|
||||
body { font-family: Georgia, serif; font-size: 18px; max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
h1 { font-size: 1.4em; }
|
||||
form { margin: 10px 0 20px; }
|
||||
input[type="text"] {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 16px;
|
||||
padding: 8px;
|
||||
width: 80%;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
input[type="submit"] {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 16px;
|
||||
padding: 8px 16px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { padding: 8px 0; border-bottom: 1px solid #ccc; }
|
||||
a { color: #000; }
|
||||
small { color: #555; }
|
||||
.count { color: #555; font-size: 0.85em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>10-K Annual Reports</h1>
|
||||
<form method="get" action="/">
|
||||
<input type="text" name="q" value="{{ query }}" placeholder="Search ticker or company name...">
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
{% if query %}
|
||||
<p class="count">{{ total }} result{{ 's' if total != 1 else '' }} for "{{ query }}"</p>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for t, c in companies.items() %}
|
||||
<li><a href="/{{ t }}">{{ t }}</a> — {{ c.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
109
tests/test_app.py
Normal file
109
tests/test_app.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""Tests for the 10-K viewer Flask app."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import app as viewer # noqa: E402
|
||||
|
||||
|
||||
SAMPLE_SUBS = {
|
||||
"filings": {
|
||||
"recent": {
|
||||
"form": ["10-K", "8-K", "10-Q", "10-K"],
|
||||
"filingDate": ["2024-11-01", "2024-10-15", "2024-08-01", "2023-11-02"],
|
||||
"primaryDocument": [
|
||||
"aapl-20240928.htm",
|
||||
"aapl-20241015.htm",
|
||||
"aapl-20240628.htm",
|
||||
"aapl-20230930.htm",
|
||||
],
|
||||
"accessionNumber": [
|
||||
"0000320193-24-000123",
|
||||
"0000320193-24-000100",
|
||||
"0000320193-24-000090",
|
||||
"0000320193-23-000050",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
viewer._filings_cache.clear()
|
||||
viewer._tickers = None
|
||||
viewer.app.config["TESTING"] = True
|
||||
with viewer.app.test_client() as c:
|
||||
yield c
|
||||
|
||||
|
||||
def test_parse_filings_filters_forms():
|
||||
r = viewer.parse_filings(SAMPLE_SUBS, "0000320193", {"10-K"})
|
||||
assert len(r) == 2
|
||||
assert all(f["form"] == "10-K" for f in r)
|
||||
|
||||
|
||||
def test_parse_filings_builds_url_and_file():
|
||||
r = viewer.parse_filings(SAMPLE_SUBS, "0000320193", {"10-K", "10-Q"})
|
||||
first = r[0]
|
||||
assert first["file"] == "2024-11-01_000032019324000123.html"
|
||||
assert (
|
||||
first["url"]
|
||||
== "https://www.sec.gov/Archives/edgar/data/0000320193/000032019324000123/aapl-20240928.htm"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_filings_no_matching_forms():
|
||||
assert viewer.parse_filings(SAMPLE_SUBS, "0000320193", {"20-F"}) == []
|
||||
|
||||
|
||||
def test_clean_ereader_strips_scripts_and_handlers():
|
||||
html = b"<html><head><script>evil()</script></head><body><div onclick='x()' class='c' id='i'>hi</div></body></html>"
|
||||
out = viewer.clean_ereader(html).decode("utf-8")
|
||||
assert "<script>" not in out
|
||||
assert "onclick" not in out
|
||||
assert "class=" not in out
|
||||
assert "id=" not in out
|
||||
assert "Georgia" in out
|
||||
|
||||
|
||||
def test_get_tickers_fallback():
|
||||
viewer._tickers = None
|
||||
t = viewer.get_tickers()
|
||||
assert t["AAPL"]["cik"] == "0000320193"
|
||||
|
||||
|
||||
def test_index_default_lists_companies(client):
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
assert "AAPL" in resp.get_data(as_text=True)
|
||||
|
||||
|
||||
def test_index_search_filters(client):
|
||||
resp = client.get("/?q=apple")
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_data(as_text=True)
|
||||
assert "AAPL" in body
|
||||
assert "MSFT" not in body
|
||||
|
||||
|
||||
def test_company_renders_filings(client, monkeypatch):
|
||||
monkeypatch.setattr(viewer, "get_filings", lambda cik, forms: [
|
||||
{"form": "10-K", "date": "2024-11-01", "file": "f1.html", "url": "https://x/f1.html"},
|
||||
])
|
||||
resp = client.get("/AAPL")
|
||||
assert resp.status_code == 200
|
||||
assert "Apple Inc." in resp.get_data(as_text=True)
|
||||
assert "f1.html" in resp.get_data(as_text=True)
|
||||
|
||||
|
||||
def test_company_unknown_ticker_404(client):
|
||||
assert client.get("/NOPE").status_code == 404
|
||||
|
||||
|
||||
def test_view_unknown_ticker_404(client):
|
||||
assert client.get("/NOPE/view/f1.html").status_code == 404
|
||||
Loading…
x
Reference in New Issue
Block a user