Skip to content

Commit f2e2f37

Browse files
committed
Add comprehensive tests for _repr_html_() method across response models
- Introduced tests for Filing, CompanyInfo, Submission, Fact, Facts, and SearchResult models. - Verified HTML output for various attributes including form type, filing date, and company name. - Ensured proper handling of special characters and empty values in HTML rendering. - Included tests for recent filings and taxonomy summaries in CompanyInfo and Facts models respectively.
1 parent 478a5c5 commit f2e2f37

5 files changed

Lines changed: 1570 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7070
- **edgar/session.py**: `build_url()` and `make_request()` accept optional `base_url` parameter to support third-party SEC endpoints (e.g. `efts.sec.gov`).
7171
- **tests/test_search.py**: 35 unit tests for `SearchResult` model, `Search` service, `EdgarClient.search()` integration, and `build_url` base_url parameter.
7272
- **samples/use_search.py**: Sample file demonstrating full-text search (basic query, form type filtering, date ranges, result properties, pagination).
73+
- **edgar/models.py**: `_repr_html_()` on all six response models for Jupyter/notebook rendering.
74+
- `Filing`, `CompanyInfo`, `Submission`, `Fact`, `Facts`, `SearchResult` auto-render as styled HTML tables.
75+
- Helper functions `_html_kv_table()`, `_html_row_table()`, `_esc()` for XSS-safe HTML generation.
76+
- Inline CSS constants (`_TABLE_STYLE`, `_TH_STYLE`, `_TD_STYLE`, `_CAPTION_STYLE`) for consistent styling across Jupyter Lab, Notebook, VS Code, and Colab.
77+
- **tests/test_repr_html.py**: 35 unit tests for `_repr_html_()` on all models (HTML output, key values, XSS escaping, edge cases).
78+
- **samples/demo_jupyter_rendering.ipynb**: Jupyter notebook demonstrating auto-rendering for all model types and DataFrame conversion.
7379

7480
### Changed
7581

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,4 @@ pay monthly fees.
170170
If you'd like to watch more of my content, feel free to visit my YouTube channel [Sigma Coding](https://www.youtube.com/c/SigmaCoding).
171171

172172
**Questions:**
173-
If you have questions please feel free to reach out to me at [coding.sigma@gmail.com](mailto:coding.sigma@gmail.com?subject=[GitHub]%20Fred%20Library)
173+
If you have questions please feel free to reach out to me at [coding.sigma@gmail.com](mailto:coding.sigma@gmail.com?subject=[GitHub]%20Sec%20Library)

edgar/models.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6+
from html import escape as _html_escape
7+
8+
9+
_TABLE_STYLE = (
10+
"border-collapse:collapse;font-family:monospace;font-size:13px;"
11+
)
12+
_TH_STYLE = (
13+
"text-align:left;padding:4px 10px;border:1px solid #ccc;"
14+
"background:#f4f4f4;font-weight:600;"
15+
)
16+
_TD_STYLE = "text-align:left;padding:4px 10px;border:1px solid #ccc;"
17+
_CAPTION_STYLE = (
18+
"caption-side:top;text-align:left;font-weight:700;"
19+
"font-size:14px;padding-bottom:4px;"
20+
)
21+
22+
23+
def _html_kv_table(pairs: list[tuple[str, str]], caption: str = "") -> str:
24+
"""Build an HTML key-value table (two columns: Field / Value)."""
25+
rows = "".join(
26+
f"<tr><th style=\"{_TH_STYLE}\">{_html_escape(str(k))}</th>"
27+
f"<td style=\"{_TD_STYLE}\">{v}</td></tr>"
28+
for k, v in pairs
29+
)
30+
cap = f"<caption style=\"{_CAPTION_STYLE}\">{_html_escape(caption)}</caption>" if caption else ""
31+
return f"<table style=\"{_TABLE_STYLE}\">{cap}{rows}</table>"
32+
33+
34+
def _html_row_table(
35+
headers: list[str],
36+
rows: list[list[str]],
37+
caption: str = "",
38+
) -> str:
39+
"""Build an HTML table with column headers and multiple data rows."""
40+
hdr = "".join(f"<th style=\"{_TH_STYLE}\">{_html_escape(h)}</th>" for h in headers)
41+
body = ""
42+
for row in rows:
43+
cells = "".join(f"<td style=\"{_TD_STYLE}\">{v}</td>" for v in row)
44+
body += f"<tr>{cells}</tr>"
45+
cap = f"<caption style=\"{_CAPTION_STYLE}\">{_html_escape(caption)}</caption>" if caption else ""
46+
return f"<table style=\"{_TABLE_STYLE}\">{cap}<tr>{hdr}</tr>{body}</table>"
47+
48+
49+
def _esc(value) -> str:
50+
"""Escape a value for safe HTML display."""
51+
return _html_escape(str(value))
652

753

854
def _require_pandas():
@@ -73,6 +119,23 @@ def accession_number(self) -> str:
73119
def __repr__(self) -> str:
74120
return f"<Filing form={self.form_type!r} date={self.filing_date[:10]!r} title={self.title!r}>"
75121

122+
def _repr_html_(self) -> str:
123+
url_cell = (
124+
f"<a href=\"{_esc(self.url)}\">{_esc(self.url)}</a>"
125+
if self.url else ""
126+
)
127+
return _html_kv_table(
128+
[
129+
("Form Type", _esc(self.form_type)),
130+
("Filing Date", _esc(self.filing_date[:10])),
131+
("Accession #", _esc(self.accession_number)),
132+
("Title", _esc(self.title)),
133+
("Summary", _esc(self.summary)),
134+
("URL", url_cell),
135+
],
136+
caption="Filing",
137+
)
138+
76139

77140
@dataclass(frozen=True)
78141
class CompanyInfo:
@@ -166,6 +229,39 @@ def __repr__(self) -> str:
166229
f"<CompanyInfo name={self.name!r} cik={self.cik!r} tickers={ticker_str!r}>"
167230
)
168231

232+
def _repr_html_(self) -> str:
233+
ticker_str = ", ".join(self.tickers) if self.tickers else "—"
234+
exchange_str = ", ".join(self.exchanges) if self.exchanges else "—"
235+
info = _html_kv_table(
236+
[
237+
("Name", _esc(self.name)),
238+
("CIK", _esc(self.cik)),
239+
("Entity Type", _esc(self.entity_type)),
240+
("SIC", f"{_esc(self.sic)}{_esc(self.sic_description)}"),
241+
("Tickers", _esc(ticker_str)),
242+
("Exchanges", _esc(exchange_str)),
243+
("Fiscal Year End", _esc(self.fiscal_year_end)),
244+
],
245+
caption="Company Info",
246+
)
247+
subs = self.recent_submissions[:10]
248+
if subs:
249+
rows = [
250+
[
251+
_esc(s.form),
252+
_esc(s.filing_date),
253+
_esc(s.accession_number),
254+
_esc(s.primary_doc_description),
255+
]
256+
for s in subs
257+
]
258+
info += _html_row_table(
259+
["Form", "Filing Date", "Accession #", "Description"],
260+
rows,
261+
caption=f"Recent Filings (showing {len(subs)})",
262+
)
263+
return info
264+
169265

170266
@dataclass(frozen=True)
171267
class Submission:
@@ -234,6 +330,22 @@ def size(self) -> int:
234330
def __repr__(self) -> str:
235331
return f"<Submission form={self.form!r} date={self.filing_date!r} accession={self.accession_number!r}>"
236332

333+
def _repr_html_(self) -> str:
334+
return _html_kv_table(
335+
[
336+
("Form", _esc(self.form)),
337+
("Filing Date", _esc(self.filing_date)),
338+
("Report Date", _esc(self.report_date)),
339+
("Accession #", _esc(self.accession_number)),
340+
("Primary Document", _esc(self.primary_document)),
341+
("Description", _esc(self.primary_doc_description)),
342+
("XBRL", "Yes" if self.is_xbrl else "No"),
343+
("Inline XBRL", "Yes" if self.is_inline_xbrl else "No"),
344+
("Size", f"{self.size:,} bytes" if self.size else "—"),
345+
],
346+
caption="Submission",
347+
)
348+
237349

238350
@dataclass(frozen=True)
239351
class Fact:
@@ -303,6 +415,22 @@ def __repr__(self) -> str:
303415
f" form={self.form!r} fy={self.fiscal_year}>"
304416
)
305417

418+
def _repr_html_(self) -> str:
419+
val = f"{self.value:,}" if isinstance(self.value, (int, float)) else _esc(self.value)
420+
return _html_kv_table(
421+
[
422+
("End", _esc(self.end)),
423+
("Start", _esc(self.start)),
424+
("Value", val),
425+
("Form", _esc(self.form)),
426+
("Filed", _esc(self.filed)),
427+
("Fiscal Year", _esc(self.fiscal_year)),
428+
("Fiscal Period", _esc(self.fiscal_period)),
429+
("Frame", _esc(self.frame)),
430+
],
431+
caption="Fact",
432+
)
433+
306434

307435
@dataclass(frozen=True)
308436
class Facts:
@@ -512,6 +640,27 @@ def __repr__(self) -> str:
512640
f" taxonomies={tax_count} concepts={total}>"
513641
)
514642

643+
def _repr_html_(self) -> str:
644+
info = _html_kv_table(
645+
[
646+
("Entity", _esc(self.entity_name)),
647+
("CIK", _esc(self.cik)),
648+
("Taxonomies", _esc(", ".join(self.taxonomies))),
649+
],
650+
caption="Facts",
651+
)
652+
rows = [
653+
[_esc(t), str(len(self.concepts(t)))]
654+
for t in self.taxonomies
655+
]
656+
if rows:
657+
info += _html_row_table(
658+
["Taxonomy", "Concepts"],
659+
rows,
660+
caption="Taxonomy Summary",
661+
)
662+
return info
663+
515664

516665
@dataclass(frozen=True)
517666
class SearchResult:
@@ -604,6 +753,25 @@ def __repr__(self) -> str:
604753
f" company={self.company_name!r}>"
605754
)
606755

756+
def _repr_html_(self) -> str:
757+
url_cell = (
758+
f"<a href=\"{_esc(self.url)}\">{_esc(self.url)}</a>"
759+
if self.url else ""
760+
)
761+
return _html_kv_table(
762+
[
763+
("Company", _esc(self.company_name)),
764+
("CIK", _esc(self.cik)),
765+
("Form", _esc(self.form)),
766+
("Filing Date", _esc(self.filing_date)),
767+
("Accession #", _esc(self.accession_number)),
768+
("File Type", _esc(self.file_type)),
769+
("Period Ending", _esc(self.period_ending)),
770+
("URL", url_cell),
771+
],
772+
caption="Search Result",
773+
)
774+
607775

608776
def to_dataframe(items: list):
609777
"""Convert a list of model objects to a pandas DataFrame.

0 commit comments

Comments
 (0)