From fa7e3eb545e58538a808a1170b522d65e98f8992 Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Thu, 20 Aug 2026 21:11:29 +0000 Subject: [PATCH] chore: polish for public release - Real contact in SEC User-Agent (required by EDGAR) - Fix Dockerfile cache dir (was missing, build would fail) - Pin flask>=3.0,<4 - Harden clean_ereader to strip single-quoted attributes - Remove unused templates (archive.html, more.html) - Add pytest suite (10 tests) and CI workflow --- .github/workflows/test.yml | 25 +++++++++ .gitignore | 3 +- README.md | 7 +++ app.py | 10 ++-- cache/.gitkeep | 0 requirements.txt | 2 +- templates/archive.html | 26 --------- templates/more.html | 33 ----------- tests/test_app.py | 109 +++++++++++++++++++++++++++++++++++++ 9 files changed, 149 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 cache/.gitkeep delete mode 100644 templates/archive.html delete mode 100644 templates/more.html create mode 100644 tests/test_app.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e9352b0 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore index 6552aeb..0c22ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ *.pyc -cache/ .env *.log +cache/* +!cache/.gitkeep diff --git a/README.md b/README.md index f2bb3ff..382e449 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ docker run -p 5001:5001 10k-viewer 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 diff --git a/app.py b/app.py index 9d2b5f1..4abb5d6 100644 --- a/app.py +++ b/app.py @@ -9,8 +9,9 @@ app = Flask(__name__) logging.basicConfig(level=logging.INFO) log = logging.getLogger("10k") -UA = "10k-viewer/1.0 (test@test.com)" +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 @@ -155,9 +156,9 @@ def clean_ereader(html): t = html.decode("utf-8", errors="replace") t = re.sub(r']*>.*?', '', t, flags=re.DOTALL|re.I) t = re.sub(r']*>.*?', '', 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"""\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 = '' if '' in t.lower(): @@ -229,5 +230,4 @@ def view(ticker, filename): if __name__ == "__main__": - CACHE_DIR.mkdir(parents=True, exist_ok=True) app.run(host="0.0.0.0", port=5001) diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 7e10602..bbedbcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -flask +flask>=3.0,<4 diff --git a/templates/archive.html b/templates/archive.html deleted file mode 100644 index d6cae89..0000000 --- a/templates/archive.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - -{{ ticker }} - Archive Status - - - -

← Back to {{ ticker }} | All companies

-

Archive Status

-
-

{{ archived }} of {{ total }} filings archived

-{% if failed > 0 %} -

{{ failed }} failed (SEC may have rate-limited)

-{% endif %} -
-

Files saved to local cache for offline reading.

- - diff --git a/templates/more.html b/templates/more.html deleted file mode 100644 index e051728..0000000 --- a/templates/more.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - -{{ ticker }} - More Filings - - - -

← Back to filings | All companies

-

{{ ticker }} - More Filings

-
    -{% for f in filings %} -
  • -{{ f.filing_date }} -{{ f.form }} -
  • -{% else %} -
  • No more filings.
  • -{% endfor %} -
- - diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..8293121 --- /dev/null +++ b/tests/test_app.py @@ -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"
hi
" + out = viewer.clean_ereader(html).decode("utf-8") + assert "