Files
2025-12-09 12:13:01 +01:00

931 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from datetime import datetime
from functools import cached_property, lru_cache, partial
from typing import Dict, List, Optional
from rich import box, print
from rich.console import Group, Text
from rich.padding import Padding
from rich.panel import Panel
from rich.table import Table
from rich.tree import Tree
from edgar._filings import Attachment, Attachments
from edgar._markdown import MarkdownContent
from edgar.files.html import Document
from edgar.files.html_documents import HtmlDocument
from edgar.files.htmltools import ChunkedDocument, adjust_for_empty_items, chunks2df, detect_decimal_items
from edgar.financials import Financials
from edgar.formatting import datefmt
from edgar.richtools import repr_rich, rich_to_text
__all__ = [
'TenK',
'TenQ',
'TwentyF',
'CurrentReport',
'SixK',
'EightK',
'PressRelease',
'PressReleases',
'is_valid_item_for_filing'
]
class CompanyReport:
def __init__(self, filing):
self._filing = filing
@property
def filing_date(self):
return self._filing.filing_date
@property
def form(self):
return self._filing.form
@property
def company(self):
return self._filing.company
@property
def income_statement(self):
return self.financials.income_statement() if self.financials else None
@property
def balance_sheet(self):
return self.financials.balance_sheet() if self.financials else None
@property
def cash_flow_statement(self):
return self.financials.cashflow_statement() if self.financials else None
@cached_property
def financials(self):
"""Get the financials for this filing"""
return Financials.extract(self._filing)
@property
def period_of_report(self):
return self._filing.header.period_of_report
@cached_property
def chunked_document(self):
return ChunkedDocument(self._filing.html())
@property
def doc(self):
return self.chunked_document
@property
def items(self) -> List[str]:
return self.chunked_document.list_items()
def __getitem__(self, item_or_part: str):
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document[item_or_part]
return item_text
def view(self, item_or_part: str):
"""Get the Item or Part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q"""
item_text = self[item_or_part]
if item_text:
print(item_text)
def __rich__(self):
return Panel(
Group(
self._filing.__rich__(),
self.financials() or Text("No financial data available")
)
)
def __repr__(self):
return repr_rich(self.__rich__())
class FilingStructure:
def __init__(self, structure: Dict):
self.structure = structure
def get_part(self, part: str):
return self.structure.get(part.upper())
def get_item(self, item: str, part: str = None):
item = item.upper()
if part:
part_dict = self.get_part(part)
if part_dict:
return part_dict.get(item)
else:
for _, items in self.structure.items():
if item in items:
return items[item]
return None
def is_valid_item(self, item: str, part: str = None):
return self.get_item(item, part) is not None
class ItemOnlyFilingStructure(FilingStructure):
def get_part(self, part: str):
return None
def get_item(self, item: str, part: str = None):
return self.structure.get(item.upper())
class TenK(CompanyReport):
structure = FilingStructure({
"PART I": {
# special case for 10-K
# Items 1 and 2. Business and Properties
"ITEM 1": {
"Title": "Business",
"Description": "Overview of the company's business operations, products, services, and market environment."
},
"ITEM 1A": {
"Title": "Risk Factors",
"Description": "Discussion of risks and uncertainties that could materially affect the company's " +
"financial condition or results of operations."
},
"ITEM 1B": {
"Title": "Unresolved Staff Comments",
"Description": "Any comments from the SEC staff on the company's previous filings" +
"that remain unresolved."
},
"ITEM 1C": {
"Title": "Cybersecurity",
"Description": "Cybersecurity risk management, strategy, and governance disclosures."
},
"ITEM 2": {
"Title": "Properties",
"Description": "Information about the physical properties owned or leased by the company."
},
"ITEM 3": {
"Title": "Legal Proceedings",
"Description": "Details of significant ongoing legal proceedings."
},
"ITEM 4": {
"Title": "Mine Safety Disclosures",
"Description": "Relevant for mining companies, disclosures about mine safety and regulatory compliance."
}
},
"PART II": {
"ITEM 5": {
"Title": "Market for Registrants Common Equity",
"Description": "Information on the companys equity, including stock performance " +
"and shareholder matters."
},
"ITEM 6": {
"Title": "Selected Financial Data",
"Description": "Financial data summary for the last five fiscal years."
},
"ITEM 7": {
"Title": "Managements Discussion and Analysis (MD&A)",
"Description": "Managements perspective on the financial condition, changes in financial condition, " +
"and results of operations."
},
"ITEM 7A": {
"Title": "Quantitative and Qualitative Disclosures About Market Risk",
"Description": "Information on the company's exposure to market risk, such as interest rate risk, " +
"foreign currency exchange risk, commodity price risk, etc."
},
"ITEM 8": {
"Title": "Financial Statements",
"Description": "Complete audited financial statements, including balance sheet, income statement, " +
"cash flow statement, and notes to the financial statements."
},
"ITEM 9": {
"Title": "Controls and Procedures",
"Description": "Evaluation of the effectiveness of the design and operation of the companys disclosure controls and procedures."
},
"ITEM 9A": {
"Title": "Controls and Procedures",
"Description": "Evaluation of internal controls over financial reporting."
},
"ITEM 9B": {
"Title": "Other Information",
"Description": "Any other relevant information not covered in other sections."
},
"ITEM 9C": {
"Title": "Disclosure Regarding Foreign Jurisdictions That Prevent Inspections",
"Description": "Disclosure Regarding Foreign Jurisdictions That Prevent Inspections."
}
},
"PART III": {
"ITEM 10": {
"Title": "Directors, Executive Officers, and Corporate Governance",
"Description": "Information about the company's directors, executive officers, and governance policies."
},
"ITEM 11": {
"Title": "Executive Compensation",
"Description": "Details of compensation paid to key executives."
},
"ITEM 12": {
"Title": "Security Ownership of Certain Beneficial Owners and Management",
"Description": "Information about stock ownership of major shareholders, directors, and management."
},
"ITEM 13": {
"Title": "Certain Relationships and Related Transactions, and Director Independence",
"Description": "Information on transactions between the company and its directors, officers, " +
"and significant shareholders."
},
"ITEM 14": {
"Title": "Principal Accounting Fees and Services",
"Description": "Fees paid to the principal accountant and services rendered."
}
},
"PART IV": {
"ITEM 15": {
"Title": "Exhibits, Financial Statement Schedules",
"Description": "Legal documents and financial schedules that support the financial statements " +
"and disclosures."
},
"ITEM 16": {
"Title": "Form 10-K Summary",
"Description": "Form 10-K Summary"
}
}
})
def __init__(self, filing):
assert filing.form in ['10-K', '10-K/A'], f"This form should be a 10-K but was {filing.form}"
super().__init__(filing)
@property
def business(self):
return self['Item 1']
@property
def risk_factors(self):
return self['Item 1A']
@property
def management_discussion(self):
return self['Item 7']
@property
def directors_officers_and_governance(self):
return self['Item 10']
@cached_property
def chunked_document(self):
return ChunkedDocument(self._filing.html(), prefix_src=self._filing.base_dir)
@lru_cache(maxsize=1)
def id_parse_document(self, markdown:bool=False):
from edgar.files.html_documents_id_parser import ParsedHtml10K
return ParsedHtml10K().extract_html(self._filing.html(), self.structure, markdown=markdown)
def __str__(self):
return f"""TenK('{self.company}')"""
def __getitem__(self, item_or_part: str):
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document[item_or_part]
if item_text:
item_text = item_text.rstrip()
last_line = item_text.split("\n")[-1]
if re.match(r'^\b(PART\s+[IVXLC]+)\b', last_line):
item_text = item_text.rstrip(last_line)
return item_text
def get_item_with_part(self, part: str, item: str, markdown:bool=True):
if not part:
return self.id_parse_document(markdown).get(item.lower())
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document.get_item_with_part(part, item, markdown=markdown)
# remove first line or last line (redundant part information)
if not item_text or not item_text.strip():
return self.id_parse_document(markdown).get(part.lower(), {}).get(item.lower())
return item_text
def get_structure(self):
# Create the main tree
tree = Tree("📄 ")
# Get the actual items from the filing
actual_items = self.items
# Create a mapping of uppercase to actual case items
case_mapping = {item.upper(): item for item in actual_items}
# Process each part in the structure
for part, items in self.structure.structure.items():
# Create a branch for each part
part_tree = tree.add(f"[bold blue]{part}[/]")
# Add items under each part
for item_key, item_data in items.items():
# Check if this item exists in the actual filing
if item_key in case_mapping:
# Use the actual case from the filing
actual_item = case_mapping[item_key]
item_text = Text.assemble(
(f"{actual_item:<7} ", "bold green"),
(f"{item_data['Title']}", "bold"),
)
else:
# Item doesn't exist - show in grey with original structure case
item_text = Text.assemble(
(f"{item_key}: ", "dim"),
(f"{item_data['Title']}", "dim"),
)
part_tree.add(item_text)
return tree
def __rich__(self):
title = Text.assemble(
(f"{self.company}", "bold deep_sky_blue1"),
(" ", ""),
(f"{self.form}", "bold"),
)
periods = Text.assemble(
("Period ending ", "grey70"),
(f"{datefmt(self.period_of_report, '%B %d, %Y')}", "bold"),
(" filed on ", "grey70"),
(f"{datefmt(self.filing_date, '%B %d, %Y')}", "bold"),
)
panel = Panel(
Group(
periods,
Padding(" ", (1, 0, 0, 0)),
self.get_structure(),
Padding(" ", (1, 0, 0, 0)),
self.financials or Text("No financial data available", style="italic")
),
title=title,
box=box.ROUNDED,
)
return panel
class TenQ(CompanyReport):
structure = FilingStructure({
"PART I": { # Financial Information
"ITEM 1": {
"Title": "Financial Statements",
"Description": "Unaudited financial statements including balance sheets, income statements, " +
"and cash flow statements."
},
"ITEM 2": {
"Title": "Managements Discussion and Analysis of Financial Condition and Results of Operations (MD&A)",
"Description": "Managements perspective on the financial condition and results of operations."
},
"ITEM 3": {
"Title": "Quantitative and Qualitative Disclosures About Market Risk",
"Description": "Information on the company's exposure to market risk."
},
"ITEM 4": {
"Title": "Controls and Procedures",
"Description": "Evaluation of the effectiveness of disclosure controls and procedures."
}
},
"PART II": { # Other Information
"ITEM 1": {
"Title": "Legal Proceedings",
"Description": "Brief description of any significant pending legal proceedings."
},
"ITEM 1A": {
"Title": "Risk Factors",
"Description": "An update on risk factors that may affect future results."
},
"ITEM 2": {
"Title": "Unregistered Sales of Equity Securities and Use of Proceeds",
"Description": "Details of unregistered sales of equity securities."
},
"ITEM 3": {
"Title": "Defaults Upon Senior Securities",
"Description": "Information regarding any defaults on senior securities."
},
"ITEM 4": {
"Title": "Mine Safety Disclosures",
"Description": "Required for companies with mining operations."
},
"ITEM 5": {
"Title": "Other Information",
"Description": "Any other information that should be disclosed to investors."
},
"ITEM 6": {
"Title": "Exhibits",
"Description": "List of exhibits required by Item 601 of Regulation S-K."
}
}
})
def __init__(self, filing):
assert filing.form in ['10-Q', '10-Q/A'], f"This form should be a 10-Q but was {filing.form}"
super().__init__(filing)
def __str__(self):
return f"""TenQ('{self.company}')"""
def __getitem__(self, item_or_part: str):
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document[item_or_part]
return item_text
def get_item_with_part(self, part: str, item: str, markdown:bool=True):
if not part:
return self.id_parse_document(markdown).get(part.lower(), {}).get(item.lower())
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document.get_item_with_part(part, item, markdown=markdown)
# remove first line or last line (redundant part information)
if not item_text or not item_text.strip():
return self.id_parse_document(markdown).get(part.lower(), {}).get(item.lower())
return item_text
@lru_cache(maxsize=1)
def id_parse_document(self, markdown:bool=True):
from edgar.files.html_documents_id_parser import ParsedHtml10Q
return ParsedHtml10Q().extract_html(self._filing.html(), self.structure, markdown=markdown)
@cached_property
def chunked_document(self):
return ChunkedDocument(self._filing.html(), prefix_src=self._filing.base_dir)
def get_structure(self):
# Create the main tree
tree = Tree("📄 ")
# Get the actual items from the filing
actual_items = self.items
# Create a mapping of uppercase to actual case items
case_mapping = {item.upper(): item for item in actual_items}
# Process each part in the structure
for part, items in self.structure.structure.items():
# Create a branch for each part
part_tree = tree.add(f"[bold blue]{part}[/]")
# Add items under each part
for item_key, item_data in items.items():
# Check if this item exists in the actual filing
if item_key in case_mapping:
# Use the actual case from the filing
actual_item = case_mapping[item_key]
item_text = Text.assemble(
(f"{actual_item:<7} ", "bold green"),
(f"{item_data['Title']}", "bold"),
)
else:
# Item doesn't exist - show in grey with original structure case
item_text = Text.assemble(
(f"{item_key}: ", "dim"),
(f"{item_data['Title']}", "dim"),
)
part_tree.add(item_text)
return tree
def __rich__(self):
title = Text.assemble(
(f"{self.company}", "bold deep_sky_blue1"),
(" ", ""),
(f"{self.form}", "bold"),
)
periods = Text.assemble(
("Period ending ", "grey70"),
(f"{datefmt(self.period_of_report, '%B %d, %Y')}", "bold"),
(" filed on ", "grey70"),
(f"{datefmt(self.filing_date, '%B %d, %Y')}", "bold"),
)
panel = Panel(
Group(
periods,
Padding(" ", (1, 0, 0, 0)),
self.get_structure(),
Padding(" ", (1, 0, 0, 0)),
self.financials or Text("No financial data available", style="italic")
),
title=title,
box=box.ROUNDED,
)
return panel
class TwentyF(CompanyReport):
structure = FilingStructure({
"PART I": {
"ITEM 1": {
"Title": "Identity of Directors, Senior Management, and Advisers",
"Description": "Information about the company's directors, senior management, and advisers."
},
"ITEM 2": {
"Title": "Offer Statistics and Expected Timetable",
"Description": "Details on recent and expected offers of securities."
},
"ITEM 3": {
"Title": "Key Information",
"Description": "Financial and other key information about the company, including risk factors and ratios."
},
"ITEM 4": {
"Title": "Information on the Company",
"Description": "Detailed information about the company's operations and properties."
},
"ITEM 4A": {
"Title": "Unresolved Staff Comments",
"Description": "Any comments from the SEC staff on the companys previous filings that " +
"remain unresolved."
}
},
"PART II": {
"ITEM 5": {
"Title": "Operating and Financial Review and Prospects",
"Description": "Managements discussion and analysis of financial condition and results of operations."
},
"ITEM 6": {
"Title": "Directors, Senior Management, and Employees",
"Description": "Information about the company's directors, senior management, and employees."
},
"ITEM 7": {
"Title": "Major Shareholders and Related Party Transactions",
"Description": "Information about major shareholders and transactions with related parties."
},
"ITEM 8": {
"Title": "Financial Information",
"Description": "Audited financial statements and supplementary financial information."
},
"ITEM 9": {
"Title": "The Offer and Listing",
"Description": "Details on the company's securities and markets where they are traded."
}
},
"PART III": {
"ITEM 10": {
"Title": "Additional Information",
"Description": "Additional information such as share capital, memoranda, and articles of association."
},
"ITEM 11": {
"Title": "Quantitative and Qualitative Disclosures About Market Risk",
"Description": "Information on the company's exposure to market risk."
},
"ITEM 12": {
"Title": "Description of Securities Other Than Equity Securities",
"Description": "Detailed information on securities other than equity."
}
},
"PART IV": {
"ITEM 13": {
"Title": "Defaults, Dividend Arrearages, and Delinquencies",
"Description": "Information about defaults on payments and arrearages."
},
"ITEM 14": {
"Title": "Material Modifications to the Rights of Security Holders and Use of Proceeds",
"Description": "Details on any modifications to the rights of security holders."
},
"ITEM 15": {
"Title": "Controls and Procedures",
"Description": "Assessment of the effectiveness of disclosure controls and internal controls over financial reporting."
},
"ITEM 16": {
"Title": "Various Disclosures",
"Description": "Includes disclosures related to audit committee financial experts, code of ethics, " +
"principal accountant fees and services, and other corporate governance matters."
}
},
"PART V": {
"ITEM 17": {
"Title": "Financial Statements",
"Description": "Financial statements prepared in accordance with or reconciled to U.S. GAAP or IFRS."
},
"ITEM 18": {
"Title": "Financial Statements",
"Description": "If different from Item 17, financial statements prepared in accordance with " +
"home country standards."
},
"ITEM 19": {
"Title": "Exhibits",
"Description": "Legal and financial documents supporting the information in the report."
}
}
})
def __init__(self, filing):
assert filing.form in ['20-F', '20-F/A'], f"This form should be a 20-F but was {filing.form}"
super().__init__(filing)
def __str__(self):
return f"""TwentyF('{self.company}')"""
class CurrentReport(CompanyReport):
structure = ItemOnlyFilingStructure({
"ITEM 1.01": {
"Title": "Entry into a Material Definitive Agreement",
"Description": "Reports any material agreement not made in the ordinary course of business."
},
"ITEM 1.02": {
"Title": "Termination of a Material Definitive Agreement",
"Description": "Reports the termination of any material agreement."
},
"ITEM 1.03": {
"Title": "Bankruptcy or Receivership",
"Description": "Reports any bankruptcy or receivership."
},
"ITEM 2.01": {"Title": "Completion of Acquisition or Disposition of Assets",
"Description": "Reports the completion of an acquisition or disposition of a significant " +
"amount of assets."},
"ITEM 2.02": {"Title": "Results of Operations and Financial Condition",
"Description": "Reports on the company's results of operations and financial condition."},
"ITEM 2.03": {
"Title": "Creation of a Direct Financial Obligation or an Obligation under an Off-Balance Sheet " +
"Arrangement of a Registrant",
"Description": "Reports the creation of a direct financial obligation."},
"ITEM 2.04": {
"Title": "Triggering Events That Accelerate or Increase a Direct Financial Obligation or an Obligation "
+ "under an Off-Balance Sheet Arrangement",
"Description": "Reports any triggering events."},
"ITEM 2.05": {"Title": "Costs Associated with Exit or Disposal Activities",
"Description": "Reports costs related to exit or disposal activities."},
"ITEM 2.06": {"Title": "Material Impairments", "Description": "Reports on any material impairments."},
"ITEM 3.01": {
"Title": "Notice of Delisting or Failure to Satisfy a Continued Listing Rule or Standard; " +
"Transfer of Listing",
"Description": "Reports on delisting or failure to satisfy listing rules."},
"ITEM 3.02": {"Title": "Unregistered Sales of Equity Securities",
"Description": "Reports on the sale of unregistered equity securities."},
"ITEM 3.03": {"Title": "Material Modification to Rights of Security Holders",
"Description": "Reports on any modifications to the rights of security holders."},
"ITEM 4.01": {"Title": "Changes in Registrant's Certifying Accountant",
"Description": "Reports any change in the company's accountant."},
"ITEM 4.02": {
"Title": "Non-Reliance on Previously Issued Financial Statements or a Related Audit Report or " +
"Completed Interim Review",
"Description": "Reports on non-reliance on previously issued financial statements."},
"ITEM 5.01": {"Title": "Changes in Control of Registrant",
"Description": "Reports changes in control of the company."},
"ITEM 5.02": {
"Title": "Departure of Directors or Certain Officers; Election of Directors; Appointment of Certain " +
"Officers",
"Description": "Compensatory Arrangements of Certain Officers: Reports any changes in the company's " +
"directors or certain officers."},
"ITEM 5.03": {"Title": "Amendments to Articles of Incorporation or Bylaws; Change in Fiscal Year",
"Description": "Reports on amendments to articles of incorporation or bylaws."},
"ITEM 5.04": {
"Title": "Temporary Suspension of Trading Under Registrants Employee Benefit Plans",
"Description": "Reports on the temporary suspension of trading under the companys employee benefit plans."
},
"ITEM 5.05": {
"Title": "Amendment to the Registrants Code of Ethics, or Waiver of a Provision of the Code of Ethics",
"Description": "Reports on amendments or waivers to the code of ethics."},
"ITEM 5.06": {"Title": "Change in Shell Company Status",
"Description": "Reports a change in the company's shell company status."},
"ITEM 5.07": {"Title": "Submission of Matters to a Vote of Security Holders",
"Description": "Reports on matters submitted to a vote of security holders."},
"ITEM 5.08": {"Title": "Shareholder Director Nominations",
"Description": "Reports on shareholder director nominations."},
"ITEM 6.01": {"Title": "ABS Informational and Computational Material",
"Description": "Reports ABS informational and computational material."},
"ITEM 6.02": {"Title": "Change of Servicer or Trustee",
"Description": "Reports on the change of servicer or trustee."},
"ITEM 6.03": {"Title": "Change in Credit Enhancement or Other External Support",
"Description": "Reports on changes in credit enhancement or external support."},
"ITEM 6.04": {"Title": "Failure to Make a Required Distribution",
"Description": "Reports on the failure to make a required distribution."},
"ITEM 6.05": {"Title": "Securities Act Updating Disclosure",
"Description": "Reports on Securities Act updating disclosure."},
"ITEM 9.01": {
"Title": "Financial Statements and Exhibits",
"Description": "Reports financial statements and other exhibits related to the events reported in the 8-K."
}
})
def __init__(self, filing):
assert filing.form in ['8-K', '8-K/A', '6-K', '6-K/A'], f"This form should be an 8-K but was {filing.form}"
super().__init__(filing)
@property
def has_press_release(self):
return self.press_releases is not None
@property
def press_releases(self):
attachments: Attachments = self._filing.attachments
# This query for press release currently includes EX-99, EX-99.1, EX-99.01 but not EX-99.2
# Here is what we think so far
html_document = "document.endswith('.htm')"
named_release = "re.match('.*RELEASE', description)"
type_ex_99 = "document_type in ['EX-99.1', 'EX-99', 'EX-99.01']"
press_release_query = f"{html_document} and ({named_release} or {type_ex_99})"
press_release_results = attachments.query(press_release_query)
if press_release_results:
return PressReleases(press_release_results)
@cached_property
def chunked_document(self):
html = self._filing.html()
if not html:
return None
decimal_chunk_fn = partial(chunks2df,
item_detector=detect_decimal_items,
item_adjuster=adjust_for_empty_items,
item_structure=self.structure)
return ChunkedDocument(html,
chunk_fn=decimal_chunk_fn)
@property
def doc(self):
return self.chunked_document
@property
def items(self) -> List[str]:
if self.chunked_document:
return self.chunked_document.list_items()
return []
def __getitem__(self, item_or_part: str):
# Show the item or part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q
item_text = self.chunked_document[item_or_part]
return item_text
def view(self, item_or_part: str):
"""Get the Item or Part from the filing document. e.g. Item 1 Business from 10-K or Part I from 10-Q"""
item_text = self[item_or_part]
if item_text:
print(item_text)
@property
def date_of_report(self):
"""Return the period of report for this filing"""
period_of_report_str = self._filing.header.period_of_report
if period_of_report_str:
period_of_report = datetime.strptime(period_of_report_str, "%Y-%m-%d")
return period_of_report.strftime("%B %d, %Y")
return ""
def _get_exhibit_content(self, exhibit: Attachment) -> Optional[str]:
"""
Get the content of the exhibit
"""
# For old filings the exhibit might not have a document. So we need to get the full text content
# from the sgml content
if exhibit.empty:
# Download the SGML document
sgml_document = self._filing.sgml().get_document_by_sequence(exhibit.sequence_number)
if sgml_document:
exhibit_content = sgml_document.text()
return exhibit_content
else:
html_content = exhibit.download()
if html_content:
document = Document.parse(html_content)
return repr_rich(document, width=200, force_terminal=False)
def _content_renderables(self):
"""Get the content of the exhibits as renderables"""
renderables = []
for exhibit in self._filing.exhibits:
# Skip binary files
if exhibit.is_binary():
continue
exhibit_content = self._get_exhibit_content(exhibit)
if exhibit_content:
# Remove text like [/she] and replace with (she) to prevent it being treated as rich markup
cleaned_content = re.sub(r'\[(/[^]]*)]', r'(\1)',exhibit_content)
title = Text.assemble(("Exhibit ", "bold gray54"), (exhibit.document_type, "bold green"))
renderables.append(Panel(cleaned_content,
title=title,
subtitle=Text(exhibit.description, style="gray54"),
box=box.SIMPLE))
return Group(*renderables)
def text(self):
"""Get the text of the EightK filing
This includes the text content of all the exhibits
"""
return rich_to_text(self._content_renderables())
def __rich__(self):
# Renderables for the panel.
renderables = []
# List the exhibits as a table
exhibit_table = Table("", "Type", "Description",
title="Exhibits", show_header=True, header_style="bold", box=box.ROUNDED)
renderables.append(exhibit_table)
for exhibit in self._filing.exhibits:
exhibit_table.add_row(exhibit.sequence_number, exhibit.document_type, exhibit.description)
panel_title = Text.assemble(
(f"{self.company}", "bold deep_sky_blue1"),
(" ", ""),
(f"{self.form}", "bold green"),
(" ", ""),
(f"{self.date_of_report}", "bold yellow")
)
# Add the content of the exhibits
renderables.append(self._content_renderables())
return Panel(
Group(*renderables),
title=panel_title,
box=box.SIMPLE
)
def __str__(self):
return f"{self.company} {self.form} {self.date_of_report}"
def __repr__(self):
return repr_rich(self.__rich__())
# Aliases fpr the current report
EightK = CurrentReport
SixK = CurrentReport
class PressReleases:
"""
Represent the attachment on an 8-K filing that could be press releases
"""
def __init__(self, attachments: Attachments):
self.attachments: Attachments = attachments
def __len__(self):
return len(self.attachments)
def __getitem__(self, item):
attachment = self.attachments.get_by_index(item)
if attachment:
return PressRelease(attachment)
def __rich__(self):
return self.attachments.__rich__()
def __repr__(self):
return repr_rich(self.__rich__())
class PressRelease:
"""
Represents a press release attachment from an 8-K filing
With the Type EX-99.1
"""
def __init__(self, attachment: Attachment):
self.attachment: Attachment = attachment
def url(self):
return self.attachment.url
@property
def document(self) -> str:
return self.attachment.document
@property
def description(self) -> str:
return self.attachment.description
@lru_cache(maxsize=1)
def html(self) -> str:
return self.attachment.download()
def text(self) -> str:
html = self.html()
if html:
return HtmlDocument.from_html(html, extract_data=False).text
def open(self):
self.attachment.open()
def view(self):
return self.to_markdown().view()
def to_markdown(self):
html = self.html()
markdown_content = MarkdownContent.from_html(html, title="8-K Press Release")
return markdown_content
def __rich__(self):
return self.to_markdown()
def __repr__(self):
return repr_rich(self.__rich__())
def is_valid_item_for_filing(filing_structure: Dict, item: str, part: str = None):
"""Return true if the item is valid"""
item = item.upper()
if part:
part_dict = filing_structure.get(part.upper())
if part_dict:
return item in part_dict
else:
for _, items in filing_structure.items():
if item in items:
return True
return False