You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""Filing-related Oracle service methods."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.oracle_client.models import (
|
|
ExhibitDocument,
|
|
ExhibitResponse,
|
|
FilingDocumentsResponse,
|
|
FilingEntry,
|
|
FilingSearchResponse,
|
|
)
|
|
|
|
# Mapping from SGML ITEM INFORMATION descriptions to SEC 8-K item numbers.
|
|
_ITEM_DESC_TO_NUMBER: dict[str, str] = {
|
|
"entry into a material definitive agreement": "1.01",
|
|
"termination of a material definitive agreement": "1.02",
|
|
"bankruptcy or receivership": "1.03",
|
|
"mine safety": "1.04",
|
|
"completion of acquisition or disposition of assets": "2.01",
|
|
"results of operations and financial condition": "2.02",
|
|
"creation of a direct financial obligation": "2.03",
|
|
"triggering events that accelerate": "2.04",
|
|
"costs associated with exit or disposal": "2.05",
|
|
"material impairments": "2.06",
|
|
"notice of delisting": "3.01",
|
|
"unregistered sales of equity securities": "3.02",
|
|
"material modification to rights": "3.03",
|
|
r"changes in registrant.s certifying accountant": "4.01",
|
|
"non-reliance on previously issued": "4.02",
|
|
"changes in control of registrant": "5.01",
|
|
"departure of directors or certain officers": "5.02",
|
|
"amendments to articles of incorporation or bylaws": "5.03",
|
|
"temporary suspension of trading": "5.04",
|
|
r"amendments to the registrant.s code of ethics": "5.05",
|
|
"change in shell company status": "5.06",
|
|
"submission of matters to a vote": "5.07",
|
|
"shareholder nominations": "5.08",
|
|
"regulation fd disclosure": "7.01",
|
|
"other events": "8.01",
|
|
"financial statements and exhibits": "9.01",
|
|
}
|
|
|
|
|
|
def extract_items_from_sgml_header(content: str) -> list[str]:
|
|
"""Extract SEC 8-K item numbers from SGML header ITEM INFORMATION fields."""
|
|
items_found: list[str] = []
|
|
matches = re.findall(r"ITEM INFORMATION:\s*(.+)", content)
|
|
for desc in matches:
|
|
desc_lower = desc.strip().lower()
|
|
for key, item_num in _ITEM_DESC_TO_NUMBER.items():
|
|
if re.search(key, desc_lower):
|
|
items_found.append(item_num)
|
|
break
|
|
return list(dict.fromkeys(items_found))
|
|
|
|
|
|
class FilingsService:
|
|
def __init__(self, client: OracleClient) -> None:
|
|
self._client = client
|
|
|
|
async def search_filings(
|
|
self,
|
|
ticker: str,
|
|
form_type: str | None = None,
|
|
start_date: str | None = None,
|
|
end_date: str | None = None,
|
|
) -> FilingSearchResponse:
|
|
params: dict[str, str] = {}
|
|
if form_type:
|
|
params["form_type"] = form_type
|
|
if start_date:
|
|
params["start_date"] = start_date
|
|
if end_date:
|
|
params["end_date"] = end_date
|
|
data = await self._client.get(f"/api/v1/filings/search/{ticker}", params=params)
|
|
# Real Oracle: {"ticker": ..., "filings": [{accession_number, form_type, ...}], "total_count": ...}
|
|
filings = [
|
|
FilingEntry(
|
|
accession_no=f["accession_number"],
|
|
form_type=f["form_type"],
|
|
filing_date=f["filing_date"],
|
|
accepted_at=f.get("accepted_at"),
|
|
primary_document=f.get("primary_document"),
|
|
description=f.get("filing_description"),
|
|
items=f.get("items", []),
|
|
)
|
|
for f in data.get("filings", [])
|
|
]
|
|
return FilingSearchResponse(
|
|
ticker=data.get("ticker", ticker),
|
|
filings=filings,
|
|
total=data.get("total_count", len(filings)),
|
|
)
|
|
|
|
async def get_documents(self, accession_no: str) -> FilingDocumentsResponse:
|
|
data = await self._client.get(f"/api/v1/filings/documents/{accession_no}")
|
|
exhibits = [ExhibitDocument.model_validate(e) for e in data.get("exhibits", [])]
|
|
return FilingDocumentsResponse(
|
|
accession_no=data.get("accession_number", accession_no),
|
|
exhibits=exhibits,
|
|
)
|
|
|
|
async def get_exhibit(
|
|
self, accession_no: str, exhibit_type: str = "EX-99.1"
|
|
) -> ExhibitResponse:
|
|
params = {"exhibit_type": exhibit_type}
|
|
data = await self._client.get(f"/api/v1/filings/exhibit/{accession_no}", params=params)
|
|
# Real Oracle: {"accession_number": ..., "exhibit_type": ..., "content": ...}
|
|
return ExhibitResponse(
|
|
accession_no=data.get("accession_number", accession_no),
|
|
exhibit_type=data.get("exhibit_type", exhibit_type),
|
|
content=data.get("content", ""),
|
|
)
|
|
|
|
async def get_filing_items(self, accession_no: str) -> list[str]:
|
|
"""Extract SEC 8-K item numbers from the complete submission text.
|
|
|
|
Fetches the full submission text (exhibit_type="") which contains the
|
|
SGML header with ITEM INFORMATION fields, then parses item numbers.
|
|
"""
|
|
try:
|
|
data = await self._client.get(
|
|
f"/api/v1/filings/exhibit/{accession_no}", params={"exhibit_type": ""}
|
|
)
|
|
content = data.get("content", "")
|
|
if not content:
|
|
return []
|
|
return extract_items_from_sgml_header(content)
|
|
except Exception:
|
|
return []
|