189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
# SPDX-FileCopyrightText: 2022-present Dwight Gunning <dgunning@gmail.com>
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
import re
|
|
from functools import lru_cache, partial
|
|
from typing import List, Optional, Union
|
|
|
|
from edgar._filings import Attachment, Attachments, Filing, FilingHeader, FilingHomepage, Filings, get_by_accession_number, get_by_accession_number_enriched, get_filings
|
|
from edgar.core import CAUTION, CRAWL, NORMAL, edgar_mode, get_identity, listify, set_identity
|
|
from edgar.current_filings import CurrentFilings, get_all_current_filings, get_current_filings, iter_current_filings_pages
|
|
from edgar.entity import (
|
|
Company,
|
|
CompanyData,
|
|
CompanyFiling,
|
|
CompanyFilings,
|
|
CompanySearchResults,
|
|
Entity,
|
|
EntityData,
|
|
find_company,
|
|
get_cik_lookup_data,
|
|
get_company_facts,
|
|
get_company_tickers,
|
|
get_entity,
|
|
get_entity_submissions,
|
|
get_icon_from_ticker,
|
|
get_ticker_to_cik_lookup,
|
|
)
|
|
from edgar.files import detect_page_breaks, mark_page_breaks
|
|
from edgar.files.html import Document
|
|
from edgar.financials import Financials, MultiFinancials
|
|
from edgar.funds import FundClass, FundCompany, FundSeries, find_fund
|
|
from edgar.funds.reports import NPORT_FORMS, FundReport
|
|
from edgar.storage import download_edgar_data, download_filings, is_using_local_storage, set_local_storage_path, use_local_storage
|
|
from edgar.storage_management import (
|
|
StorageAnalysis,
|
|
StorageInfo,
|
|
analyze_storage,
|
|
availability_summary,
|
|
check_filing,
|
|
check_filings_batch,
|
|
cleanup_storage,
|
|
clear_cache,
|
|
optimize_storage,
|
|
storage_info,
|
|
)
|
|
from edgar.thirteenf import THIRTEENF_FORMS, ThirteenF
|
|
from edgar.xbrl import XBRL
|
|
|
|
# Fix for Issue #457: Clear locale-corrupted cache files on first import
|
|
# This is a one-time operation that only runs if the marker file doesn't exist
|
|
try:
|
|
from edgar.httpclient import clear_locale_corrupted_cache
|
|
clear_locale_corrupted_cache()
|
|
except Exception:
|
|
# Silently continue if cache clearing fails - it's not critical
|
|
pass
|
|
|
|
# Another name for get_current_filings
|
|
get_latest_filings = get_current_filings
|
|
latest_filings = get_current_filings
|
|
current_filings = get_current_filings
|
|
|
|
# Fund portfolio report filings
|
|
get_fund_portfolio_filings = partial(get_filings, form=NPORT_FORMS)
|
|
|
|
# Restricted stock sales
|
|
get_restricted_stock_filings = partial(get_filings, form=[144])
|
|
|
|
# Insider transaction filings
|
|
get_insider_transaction_filings = partial(get_filings, form=[3, 4, 5])
|
|
|
|
# 13F filings - portfolio holdings
|
|
get_portfolio_holding_filings = partial(get_filings, form=THIRTEENF_FORMS)
|
|
|
|
|
|
@lru_cache(maxsize=16)
|
|
def find(search_id: Union[str, int]) -> Optional[Union[Filing, Entity, CompanySearchResults, FundCompany, FundClass, FundSeries]]:
|
|
"""This is an uber search function that can take a variety of search ids and return the appropriate object
|
|
- accession number -> returns a Filing
|
|
- CIK -> returns an Entity
|
|
- Class/Contract ID -> returns a FundClass
|
|
- Series ID -> returns a FundSeries
|
|
- Ticker -> returns a Company or a Fund if the ticker is a fund ticker
|
|
- Company name -> returns CompanySearchResults
|
|
|
|
:type: object
|
|
"""
|
|
if isinstance(search_id, int):
|
|
return Entity(search_id)
|
|
elif re.match(r"\d{10}-\d{2}-\d{6}", search_id):
|
|
return get_by_accession_number_enriched(search_id)
|
|
elif re.match(r"^\d{18}$", search_id): # accession number with no dashes
|
|
accession_number = search_id[:10] + "-" + search_id[10:12] + "-" + search_id[12:]
|
|
return get_by_accession_number_enriched(accession_number)
|
|
elif re.match(r"\d{4,10}$", search_id):
|
|
return Entity(search_id)
|
|
elif re.match(r"^[A-WYZ]{1,5}([.-][A-Z])?$", search_id): # Ticker (including dot or hyphenated)
|
|
return Entity(search_id)
|
|
elif re.match(r"^[A-Z]{4}X$", search_id): # Mutual Fund Ticker
|
|
return find_fund(search_id)
|
|
elif re.match(r"^[CS]\d+$", search_id):
|
|
return find_fund(search_id)
|
|
elif re.match(r"^\d{6,}-", search_id):
|
|
# Probably an invalid accession number
|
|
return None
|
|
else:
|
|
return find_company(search_id)
|
|
|
|
|
|
def matches_form(sec_filing: Filing,
|
|
form: Union[str, List[str]]) -> bool:
|
|
"""Check if the filing matches the forms"""
|
|
form_list = listify(form)
|
|
if sec_filing.form in form_list + [f"{f}/A" for f in form_list]:
|
|
return True
|
|
return False
|
|
|
|
|
|
class DataObjectException(Exception):
|
|
|
|
def __init__(self, filing: Filing):
|
|
self.message = f"Could not create a data object for Form {filing.form} filing: {filing.accession_no}"
|
|
super().__init__(self.message)
|
|
|
|
|
|
def obj(sec_filing: Filing) -> Optional[object]:
|
|
"""
|
|
Depending on the filing return the data object that contains the data for the filing
|
|
|
|
This usually coms from the xml associated with the filing, but it can also come from the extracted xbrl
|
|
:param sec_filing: The filing
|
|
:return:
|
|
"""
|
|
from edgar.company_reports import CurrentReport, EightK, TenK, TenQ, TwentyF
|
|
from edgar.effect import Effect
|
|
from edgar.form144 import Form144
|
|
from edgar.muniadvisors import MunicipalAdvisorForm
|
|
from edgar.offerings import FormC, FormD
|
|
from edgar.ownership import Form3, Form4, Form5, Ownership
|
|
|
|
if matches_form(sec_filing, "6-K"):
|
|
return CurrentReport(sec_filing)
|
|
if matches_form(sec_filing, "8-K"):
|
|
return EightK(sec_filing)
|
|
elif matches_form(sec_filing, "10-Q"):
|
|
return TenQ(sec_filing)
|
|
elif matches_form(sec_filing, "10-K"):
|
|
return TenK(sec_filing)
|
|
elif matches_form(sec_filing, "20-F"):
|
|
return TwentyF(sec_filing)
|
|
elif matches_form(sec_filing, THIRTEENF_FORMS):
|
|
# ThirteenF can work with either XML (2013+) or TXT (2012 and earlier) format
|
|
return ThirteenF(sec_filing)
|
|
elif matches_form(sec_filing, "144"):
|
|
return Form144.from_filing(sec_filing)
|
|
elif matches_form(sec_filing, "MA-I"):
|
|
return MunicipalAdvisorForm.from_filing(sec_filing)
|
|
elif matches_form(sec_filing, "3"):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return Form3(**Ownership.parse_xml(xml))
|
|
elif matches_form(sec_filing, "4"):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return Form4(**Ownership.parse_xml(xml))
|
|
elif matches_form(sec_filing, "5"):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return Form5(**Ownership.parse_xml(xml))
|
|
elif matches_form(sec_filing, "EFFECT"):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return Effect.from_xml(xml)
|
|
elif matches_form(sec_filing, "D"):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return FormD.from_xml(xml)
|
|
elif matches_form(sec_filing, ["C", "C-U", "C-AR", "C-TR"]):
|
|
xml = sec_filing.xml()
|
|
if xml:
|
|
return FormC.from_xml(xml, form=sec_filing.form)
|
|
|
|
elif matches_form(sec_filing, ["NPORT-P", "NPORT-EX"]):
|
|
return FundReport.from_filing(sec_filing)
|
|
|
|
filing_xbrl = sec_filing.xbrl()
|
|
if filing_xbrl:
|
|
return filing_xbrl
|